diff --git a/.circleci/config.yml b/.circleci/config.yml index 94f319d9..da1054c3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,7 +38,7 @@ jobs: command: | curl -L https://get.web3j.io | bash source $HOME/.web3j/source.sh - ./get_contracts.sh + ./get_dependencies.sh # run tests! - run: name: Run tests diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..58da6937 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.svg binary diff --git a/.gitignore b/.gitignore index b6e7a2bf..5ff94cca 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,7 @@ hs_err_pid* # maven build files target + +# vim specific files *.swp .vim-project diff --git a/README.md b/README.md index 430867d3..dc555d98 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ ## Class diagram -![Class diagram](https://www.plantuml.com/plantuml/svg/VP9HJy8m58NV-olkupJ07v0G0aOpZMWOFXaVBkrbDAajQMz75lM_kusH30O-zlRqdFkTT1eOFSSl8mHhDWIPjdaqw3MN2s9umW8XktyMGbiclq59y4AC2XapTXvpWcy1i2wPFZuX9qxUbob4hs_knA-G1aE0TBS9Pu_4kSduPpIwA6mzbfJhmBwSEyiU9JUfhplMpg8PP-GBBQaLOJsTrCjScC_AL2KP-ueJdCzJDO3s50xYL3MhNm1-ywxGYdoJtLeVxpffnr7IgU2u_hcLw7atHLoLzseO3c-lgg7Nyd_gBd5BCZUQxA7gLGtuw7SouxXE7gALTPdZ-HQj9JE0rGIaivLlb5LMHGvGEAqWR2ChjxSBj-_sCSD09o7YDB9feI_g4TP0VPcOabNLf_u3) -[Source code](https://www.planttext.com/?text=VP9HJy8m58NV-olkupJ07v0G0aOpZMWOFXaVBkrbDAajQMz75lM_kusH30O-zlRqdFkTT1eOFSSl8mHhDWIPjdaqw3MN2s9umW8XktyMGbiclq59y4AC2XapTXvpWcy1i2wPFZuX9qxUbob4hs_knA-G1aE0TBS9Pu_4kSduPpIwA6mzbfJhmBwSEyiU9JUfhplMpg8PP-GBBQaLOJsTrCjScC_AL2KP-ueJdCzJDO3s50xYL3MhNm1-ywxGYdoJtLeVxpffnr7IgU2u_hcLw7atHLoLzseO3c-lgg7Nyd_gBd5BCZUQxA7gLGtuw7SouxXE7gALTPdZ-HQj9JE0rGIaivLlb5LMHGvGEAqWR2ChjxSBj-_sCSD09o7YDB9feI_g4TP0VPcOabNLf_u3) +![Class diagram](./docs/class-diagram.svg) +[Source code](./docs/class-diagram.plantuml) ## How to build @@ -14,7 +14,7 @@ Install dependencies for the first time: ``` curl -L https://get.web3j.io | bash source $HOME/.web3j/source.sh -./get_contracts.sh +./get_dependencies.sh ``` Build and test: diff --git a/docs/class-diagram.plantuml b/docs/class-diagram.plantuml new file mode 100644 index 00000000..a00f095e --- /dev/null +++ b/docs/class-diagram.plantuml @@ -0,0 +1,152 @@ +@startuml + +title SingularityNet Java SDK + +package io.singularitynet.sdk.ethereum { + + interface WithAddress { + Address getAddress(); + } + + interface Signer { + byte[] sign(byte[] message); + } + WithAddress <|-- Signer + + class PrivateKeyIdentity + Signer <|.. PrivateKeyIdentity + + class MnemonicIdentity + PrivateKeyIdentity <|-- MnemonicIdentity + +} + + +package io.singularitynet.sdk.registry { + + interface MetadataStorage { + byte[] get(URI uri); + } + class IpfsMetadataStorage + MetadataStorage <|.. IpfsMetadataStorage + + class RegistryContract { + Optional getOrganizationById(String orgId); + Optional getServiceRegistrationById(String orgId, String serviceId); + } + + interface MetadataProvider { + ServiceMetadata getServiceMetadata(); + } + class RegistryMetadataProvider + MetadataProvider <|.. RegistryMetadataProvider + RegistryMetadataProvider *-- RegistryContract + RegistryMetadataProvider *-- MetadataStorage + +} + +package io.singularitynet.sdk.daemon { + + interface DaemonConnection { + T getGrpcStub(Function constructor); + void setClientCallsInterceptor(ClientInterceptor interceptor); + void shutdownNow(); + } + + class FirstEndpointDaemonConnection + DaemonConnection <|.. FirstEndpointDaemonConnection + FirstEndpointDaemonConnection o-- MetadataProvider + + class PaymentChannelStateService { + PaymentChannelStateReply getChannelState(BigInteger channelId); + } + PaymentChannelStateService o-- Signer + PaymentChannelStateService -- DaemonConnection + + interface Payment { + void toMetadata(Metadata headers); + } + +} + +package io.singularitynet.sdk.mpe { + class MultiPartyEscrowContract { + Optional getChannelById(BigInteger channelId); + Address getContractAddress(); + } + + interface PaymentChannelProvider { + PaymentChannel getChannelById(BigInteger channelId); + } + + class AskDaemonFirstPaymentChannelProvider + PaymentChannelProvider <|.. AskDaemonFirstPaymentChannelProvider + AskDaemonFirstPaymentChannelProvider *-- MultiPartyEscrowContract + AskDaemonFirstPaymentChannelProvider *-- PaymentChannelStateService + + class EscrowPayment + Payment ()- EscrowPayment + +} + +package io.singularitynet.sdk.client { + + interface Configuration { + String getEthereumJsonRpcEndpoint(); + URL getIpfsUrl(); + SignerType getSignerType(); + String getSignerMnemonic(); + byte[] getSignerPrivateKey(); + } + + class JsonConfiguration + Configuration <|.. JsonConfiguration + + interface DependencyFactory { + Web3j getWeb3j(); + ContractGasProvider getContractGasProvider(Web3j web3j); + IPFS getIpfs(); + Signer getSigner(); + } + + class ConfigurationDependencyFactory + DependencyFactory <|.. ConfigurationDependencyFactory + + interface PaymentStrategy { + Payment getPayment(GrpcCallParameters parameters, ServiceClient serviceClient); + } + + class FixedPaymentChannelPaymentStrategy + PaymentStrategy <|.. FixedPaymentChannelPaymentStrategy + + class Sdk { + Sdk(Configuration config); + Sdk(DependencyFactory factory); + + ServiceClient newServiceClient(String orgId, String serviceId, String endpointGroupName, PaymentStrategy paymentStrategy); + void shutdown(); + } + Sdk -- Configuration + Sdk -- ServiceClient + Sdk -- DependencyFactory + + interface ServiceClient { + MetadataProvider getMetadataProvider(); + PaymentChannelProvider getPaymentChannelProvider(); + Signer getSigner(); + T getGrpcStub(Function constructor); + void shutdownNow(); + } + + class BaseServiceClient + ServiceClient <|.. BaseServiceClient + BaseServiceClient *-- DaemonConnection + BaseServiceClient *-- MetadataProvider + BaseServiceClient *-- PaymentChannelProvider + BaseServiceClient *-- PaymentStrategy + BaseServiceClient *-- Signer + +} + +@enduml + diff --git a/docs/class-diagram.svg b/docs/class-diagram.svg new file mode 100644 index 00000000..9d7e1246 --- /dev/null +++ b/docs/class-diagram.svg @@ -0,0 +1,220 @@ + +SingularityNet Java SDKio.singularitynet.sdk.ethereumio.singularitynet.sdk.registryio.singularitynet.sdk.daemonio.singularitynet.sdk.mpeio.singularitynet.sdk.clientWithAddressAddress getAddress();Signerbyte[] sign(byte[] message);PrivateKeyIdentityMnemonicIdentityMetadataStoragebyte[] get(URI uri);IpfsMetadataStorageRegistryContractOptional<OrganizationRegistration> getOrganizationById(String orgId);Optional<ServiceRegistration> getServiceRegistrationById(String orgId, String serviceId);MetadataProviderServiceMetadata getServiceMetadata();RegistryMetadataProviderDaemonConnection<T> T getGrpcStub(Function<Channel, T> constructor);void setClientCallsInterceptor(ClientInterceptor interceptor);void shutdownNow();FirstEndpointDaemonConnectionPaymentChannelStateServicePaymentChannelStateReply getChannelState(BigInteger channelId);Paymentvoid toMetadata(Metadata headers);MultiPartyEscrowContractOptional<PaymentChannel> getChannelById(BigInteger channelId);Address getContractAddress();PaymentChannelProviderPaymentChannel getChannelById(BigInteger channelId);AskDaemonFirstPaymentChannelProviderEscrowPaymentPaymentConfigurationString getEthereumJsonRpcEndpoint();URL getIpfsUrl();SignerType getSignerType();String getSignerMnemonic();byte[] getSignerPrivateKey();JsonConfigurationDependencyFactoryWeb3j getWeb3j();ContractGasProvider getContractGasProvider(Web3j web3j);IPFS getIpfs();Signer getSigner();ConfigurationDependencyFactoryPaymentStrategy<ReqT, RespT> Payment getPayment(GrpcCallParameters<ReqT, RespT> parameters, ServiceClient serviceClient);FixedPaymentChannelPaymentStrategySdkSdk(Configuration config);Sdk(DependencyFactory factory);ServiceClient newServiceClient(String orgId, String serviceId, String endpointGroupName, PaymentStrategy paymentStrategy);void shutdown();ServiceClientMetadataProvider getMetadataProvider();PaymentChannelProvider getPaymentChannelProvider();Signer getSigner();T getGrpcStub(Function<Channel, T> constructor);void shutdownNow();BaseServiceClient \ No newline at end of file diff --git a/docs/design-decisions.md b/docs/design-decisions.md new file mode 100644 index 00000000..52bd1d4c --- /dev/null +++ b/docs/design-decisions.md @@ -0,0 +1,16 @@ +# To do + +# To not do + +## Add interface to facade Web3j and Ethereum + +To replace web3j entirely it is required to either write a lot of boilerplate +code or expose web3j explicitly. Thus such interface doesn't add any value but +adds compexity. It also make interface ambiguous: you need to decide use web3j +directly or get web3j from interface and use it. + +## Make MPE contract address part of a channel id + +Main usecase is to work with single MPE contract copy. Two or more MPE contracts +may be needed for rare channel migration usecases. So it will make API more +complex but doesn't have big value. diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..664bc9f7 --- /dev/null +++ b/example/README.md @@ -0,0 +1,77 @@ +# Running example + +## Ethereum identity preparation + +1. Create Ethereum wallet in Ropsten network, get ETH and AGI, following +instructions on https://dev.singularitynet.io/docs/setup/create-a-wallet/ + +2. Install `snet-cli` tool (https://github.com/singnet/snet-cli) + +```sh +$ sudo pip3 install snet-cli +``` + +3. Export private key from Metamask account and add it to `snet-cli` + +```sh +$ snet identity create test-user key \ + --network ropsten \ + --private-key "" +$ snet identity test-user +``` + +4. Deposit AGI to MultiPartyEscrow contract + +```sh +$ snet account deposit 10 -y --gas-price 380000000000 +``` + +5. Check account balance + +```sh +$ snet account balance +``` + +Result should look like: +``` + account: + ETH: 0.96804863 + AGI: 0 + MPE: 10 +``` + +## Run application + +1. Create payment channel with SingularityNet + +```sh +$ snet channel open-init snet default_group 0.1 +7days --gas-price 380000000000 +``` + +`snet-cli` should return you the payment channel id. + +2. Run application + +```sh +$ mvn exec:java -Dexec.mainClass="io.singularitynet.sdk.example.CntkImageRecognition" -Dexec.args=" " +``` + +# Create your own service client + +1. Create new maven project, see https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html + +2. Get service API + +```sh +$ mkdir -p src/main/proto +$ snet service get-api-registry snet cntk-image-recon src/main/proto +``` + +3. Add Java package option + +```sh +$ echo 'option java_package = "recognition";' >> src/main/proto/image_recon.proto +``` + +4. Write Java client app using sdk, see CntkImageRecognition.java as example + diff --git a/example/pom.xml b/example/pom.xml new file mode 100644 index 00000000..fb0e4ead --- /dev/null +++ b/example/pom.xml @@ -0,0 +1,178 @@ + + + + 4.0.0 + + io.singularitynet + snet-sdk-java-example + 1.0-SNAPSHOT + + snet-sdk-java-example + SingularityNet Java SDK usage example + + http://dev.singularitynet.io + + + UTF-8 + 1.8 + 1.8 + + + 3.5.1 + 1.20.0 + + + + + + io.singularitynet + snet-sdk-java + 1.0-SNAPSHOT + + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + io.grpc + grpc-netty-shaded + + + io.grpc + grpc-protobuf + + + io.grpc + grpc-stub + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + junit + junit + 4.12 + test + + + + + + + + + io.grpc + grpc-bom + ${grpc.version} + pom + import + + + + + + + + + kr.motd.maven + os-maven-plugin + 1.6.2 + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.4.0 + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + + grpc-java + + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + true + + + + + + compile + compile-custom + + + + + + + + + + + maven-clean-plugin + 3.1.0 + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + false + + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + + + + + + maven-project-info-reports-plugin + + + + diff --git a/example/src/main/java/io/singularitynet/sdk/example/CntkImageRecognition.java b/example/src/main/java/io/singularitynet/sdk/example/CntkImageRecognition.java new file mode 100644 index 00000000..9c26f9f2 --- /dev/null +++ b/example/src/main/java/io/singularitynet/sdk/example/CntkImageRecognition.java @@ -0,0 +1,62 @@ +package io.singularitynet.sdk.example; + +import java.math.BigInteger; + +import io.singularitynet.sdk.client.Configuration; +import io.singularitynet.sdk.client.JsonConfiguration; +import io.singularitynet.sdk.client.Sdk; +import io.singularitynet.sdk.client.PaymentStrategy; +import io.singularitynet.sdk.client.FixedPaymentChannelPaymentStrategy; +import io.singularitynet.sdk.client.ServiceClient; + +import recognition.RecognizerGrpc; +import recognition.RecognizerGrpc.RecognizerBlockingStub; +import recognition.ImageRecon.Input; +import recognition.ImageRecon.Output; + +public class CntkImageRecognition { + + public static void main(String[] args) throws Exception { + String privateKey = args[0]; + BigInteger channelId = new BigInteger(args[1]); + + String json = "{" + + "\"ethereum_json_rpc_endpoint\": \"https://ropsten.infura.io\", " + + "\"ipfs_url\": \"http://ipfs.singularitynet.io:80\"," + + "\"signer_type\": \"PRIVATE_KEY\"," + + "\"signer_private_key_base64\": \"" + hexToBase64(privateKey) + "\"" + + "}"; + Configuration config = new JsonConfiguration(json); + + Sdk sdk = new Sdk(config); + try { + + PaymentStrategy paymentStrategy = new FixedPaymentChannelPaymentStrategy( + channelId); + ServiceClient serviceClient = sdk.newServiceClient("snet", "cntk-image-recon", + "default_group", paymentStrategy); + try { + + RecognizerBlockingStub stub = serviceClient.getGrpcStub(RecognizerGrpc::newBlockingStub); + Input input = Input.newBuilder() + .setModel("ResNet152") + .setImgPath("https://d2z4fd79oscvvx.cloudfront.net/0027071_1_single_rose_385.jpeg") + .build(); + Output output = stub.flowers(input); + System.out.println("Response received: " + output); + + } finally { + serviceClient.shutdownNow(); + } + + } finally { + sdk.shutdown(); + } + } + + private static String hexToBase64(String hex) { + return io.singularitynet.sdk.common.Utils.bytesToBase64( + io.singularitynet.sdk.common.Utils.hexToBytes(hex)); + } + +} diff --git a/example/src/main/java/io/singularitynet/sdk/example/ExampleService.java b/example/src/main/java/io/singularitynet/sdk/example/ExampleService.java new file mode 100644 index 00000000..cc67ac69 --- /dev/null +++ b/example/src/main/java/io/singularitynet/sdk/example/ExampleService.java @@ -0,0 +1,63 @@ +package io.singularitynet.sdk.example; + +import java.math.BigInteger; + +import io.singularitynet.sdk.client.Configuration; +import io.singularitynet.sdk.client.JsonConfiguration; +import io.singularitynet.sdk.client.Sdk; +import io.singularitynet.sdk.client.PaymentStrategy; +import io.singularitynet.sdk.client.FixedPaymentChannelPaymentStrategy; +import io.singularitynet.sdk.client.ServiceClient; + +import example.service.CalculatorGrpc; +import example.service.CalculatorGrpc.CalculatorBlockingStub; +import example.service.ExampleService.Numbers; +import example.service.ExampleService.Result; + +public class ExampleService { + + public static void main(String[] args) throws Exception { + String privateKey = args[0]; + BigInteger channelId = new BigInteger(args[1]); + + String json = "{" + + "\"ethereum_json_rpc_endpoint\": \"https://ropsten.infura.io\", " + + "\"ipfs_url\": \"http://ipfs.singularitynet.io:80\"," + + "\"signer_type\": \"PRIVATE_KEY\"," + + "\"signer_private_key_base64\": \"" + hexToBase64(privateKey) + "\"" + + "}"; + Configuration config = new JsonConfiguration(json); + + Sdk sdk = new Sdk(config); + try { + + PaymentStrategy paymentStrategy = new FixedPaymentChannelPaymentStrategy( + channelId); + ServiceClient serviceClient = sdk.newServiceClient("snet", "example-service", + "default_group", paymentStrategy); + try { + + CalculatorBlockingStub stub = serviceClient.getGrpcStub(CalculatorGrpc::newBlockingStub); + + Numbers numbers = Numbers.newBuilder() + .setA(7) + .setB(6) + .build(); + Result result = stub.mul(numbers); + System.out.println("Response received: " + result); + + } finally { + serviceClient.shutdownNow(); + } + + } finally { + sdk.shutdown(); + } + } + + private static String hexToBase64(String hex) { + return io.singularitynet.sdk.common.Utils.bytesToBase64( + io.singularitynet.sdk.common.Utils.hexToBytes(hex)); + } + +} diff --git a/example/src/main/proto/example_service.proto b/example/src/main/proto/example_service.proto new file mode 100644 index 00000000..5f22708e --- /dev/null +++ b/example/src/main/proto/example_service.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package example_service; + +message Numbers { + float a = 1; + float b = 2; +} + +message Result { + float value = 1; +} + +service Calculator { + rpc add(Numbers) returns (Result) {} + rpc sub(Numbers) returns (Result) {} + rpc mul(Numbers) returns (Result) {} + rpc div(Numbers) returns (Result) {} +} +option java_package = "example.service"; diff --git a/example/src/main/proto/image_recon.proto b/example/src/main/proto/image_recon.proto new file mode 100644 index 00000000..c337557d --- /dev/null +++ b/example/src/main/proto/image_recon.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +message Input { + string model = 1; + string img_path = 2; +} + +message Output { + string delta_time = 1; + string top_5 = 2; +} + +service Recognizer { + rpc flowers (Input) returns (Output) {} + rpc dogs (Input) returns (Output) {} +} +option java_package = "recognition"; diff --git a/get_contracts.sh b/get_dependencies.sh similarity index 90% rename from get_contracts.sh rename to get_dependencies.sh index eb4ca524..de33fd9b 100755 --- a/get_contracts.sh +++ b/get_dependencies.sh @@ -12,11 +12,13 @@ tar -xzf ./singularitynet-platform-contracts.tgz mv package singularitynet-platform-contracts cd singularitynet-platform-contracts -mkdir ../generated-sources/sol/java +mkdir -p ../generated-sources/sol/java output=../generated-sources/sol/java package=io.singularitynet.sdk.contracts web3j solidity generate -a ./abi/MultiPartyEscrow.json --outputDir $output --package $package #--solidityTypes web3j solidity generate -a ./abi/Registry.json --outputDir $output --package $package +mkdir -p ../resources +cp -R ./networks ../resources/ cd .. snet_daemon_version=2.0.2 @@ -28,3 +30,5 @@ mv ./snet-daemon-v$snet_daemon_version-linux-amd64 snet-daemon sed -i '8s/^/option java_package = "io.singularitynet.daemon.escrow";\n/' snet-daemon/proto/state_service.proto sed -i '2s/^/option java_package = "io.singularitynet.daemon.escrow";\n/' snet-daemon/proto/control_service.proto sed -i '2s/^/option java_package = "io.singularitynet.daemon.configuration";\n/' snet-daemon/proto/configuration_service.proto +mkdir -p ./proto +cp snet-daemon/proto/state_service.proto ./proto diff --git a/pom.xml b/pom.xml index cc8f0843..b7417ecc 100644 --- a/pom.xml +++ b/pom.xml @@ -84,6 +84,11 @@ 1.2.1 + + com.google.guava + guava + 28.1-android + @@ -136,6 +141,13 @@ + + + + ${project.build.directory}/resources + + + org.xolstice.maven.plugins @@ -144,11 +156,14 @@ com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} - + grpc-java io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + true + + ${project.build.directory}/proto @@ -205,6 +220,12 @@ + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + @@ -233,6 +254,9 @@ maven-surefire-plugin 2.22.1 + + false + maven-jar-plugin diff --git a/src/main/java/io/singularitynet/sdk/client/BaseServiceClient.java b/src/main/java/io/singularitynet/sdk/client/BaseServiceClient.java index 8f95e336..1bd85ca8 100644 --- a/src/main/java/io/singularitynet/sdk/client/BaseServiceClient.java +++ b/src/main/java/io/singularitynet/sdk/client/BaseServiceClient.java @@ -1,45 +1,151 @@ package io.singularitynet.sdk.client; -import io.grpc.*; import java.util.function.Function; -import java.net.URL; +import java.util.function.Consumer; +import io.grpc.*; +import io.singularitynet.sdk.daemon.DaemonConnection; +import io.singularitynet.sdk.daemon.Payment; import io.singularitynet.sdk.registry.MetadataProvider; import io.singularitynet.sdk.registry.ServiceMetadata; +import io.singularitynet.sdk.mpe.PaymentChannelProvider; +import io.singularitynet.sdk.ethereum.Signer; +/** + * The class is responsible for providing all necessary facilities to call + * a platform service. + */ public class BaseServiceClient implements ServiceClient { + private final DaemonConnection daemonConnection; private final MetadataProvider metadataProvider; - private ManagedChannel channel; + private final PaymentChannelProvider paymentChannelProvider; + private final PaymentStrategy paymentStrategy; + private final Signer signer; - public BaseServiceClient(MetadataProvider metadataProvider) { + /** + * Constructor. + * @param daemonConnection provides live gRPC connection. + * @param metadataProvider provides the service related metadata. + * @param paymentChannelProvider provides the payment channel state. + * @param paymentStrategy provides payment for the client call. + * @param signer signs payments. + */ + public BaseServiceClient( + DaemonConnection daemonConnection, + MetadataProvider metadataProvider, + PaymentChannelProvider paymentChannelProvider, + PaymentStrategy paymentStrategy, + Signer signer) { + this.daemonConnection = daemonConnection; + this.daemonConnection.setClientCallsInterceptor(new PaymentClientInterceptor(this, paymentStrategy)); this.metadataProvider = metadataProvider; + this.paymentChannelProvider = paymentChannelProvider; + this.paymentStrategy = paymentStrategy; + this.signer = signer; + } + + @Override + public MetadataProvider getMetadataProvider() { + return metadataProvider; + } + + @Override + public PaymentChannelProvider getPaymentChannelProvider() { + return paymentChannelProvider; + } + + @Override + public Signer getSigner() { + return signer; } @Override public T getGrpcStub(Function constructor) { - return constructor.apply(getChannelLazy()); + return daemonConnection.getGrpcStub(constructor); } @Override public void shutdownNow() { - channel.shutdownNow(); + daemonConnection.shutdownNow(); } - private ManagedChannel getChannelLazy() { - if (channel == null) { - channel = getChannel(); + private static class PaymentClientInterceptor implements ClientInterceptor { + + private final ServiceClient serviceClient; + private final PaymentStrategy paymentStrategy; + + public PaymentClientInterceptor(ServiceClient serviceClient, PaymentStrategy paymentStrategy) { + this.serviceClient = serviceClient; + this.paymentStrategy = paymentStrategy; } - return channel; + + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + final Payment payment = paymentStrategy.getPayment( + new GrpcCallParameters<>(method, callOptions, next), + serviceClient); + return new ClientCallWrapper<>(next.newCall(method, callOptions), + headers -> payment.toMetadata(headers)); + } + } - private ManagedChannel getChannel() { - ServiceMetadata serviceMetadata = metadataProvider.getServiceMetadata(); - URL url = serviceMetadata.getEndpointGroups().get(0).getEndpoints().get(0); - return ManagedChannelBuilder - .forAddress(url.getHost(), url.getPort()) - .usePlaintext() - .build(); + private static class ClientCallWrapper extends ClientCall { + + private final ClientCall delegate; + private Consumer metadataUpdater; + + public ClientCallWrapper(ClientCall delegate, + Consumer metadataUpdater) { + this.delegate = delegate; + this.metadataUpdater = metadataUpdater; + } + + @Override + public void cancel(String message, Throwable cause) { + delegate.cancel(message, cause); + } + + @Override + public Attributes getAttributes() { + return delegate.getAttributes(); + } + + @Override + public void halfClose() { + delegate.halfClose(); + } + + @Override + public boolean isReady() { + return delegate.isReady(); + } + + @Override + public void request(int numMessages) { + delegate.request(numMessages); + } + + @Override + public void sendMessage(ReqT message) { + delegate.sendMessage(message); + } + + @Override + public void setMessageCompression(boolean enabled) { + delegate.setMessageCompression(enabled); + } + + @Override + public void start(ClientCall.Listener responseListener, Metadata headers) { + metadataUpdater.accept(headers); + delegate.start(responseListener, headers); + } + } } diff --git a/src/main/java/io/singularitynet/sdk/client/Configuration.java b/src/main/java/io/singularitynet/sdk/client/Configuration.java new file mode 100644 index 00000000..c1bc4ac6 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/client/Configuration.java @@ -0,0 +1,20 @@ +package io.singularitynet.sdk.client; + +import java.net.URL; + +import io.singularitynet.sdk.ethereum.Signer; + +public interface Configuration { + + static enum SignerType { + MNEMONIC, + PRIVATE_KEY + } + + String getEthereumJsonRpcEndpoint(); + URL getIpfsUrl(); + SignerType getSignerType(); + String getSignerMnemonic(); + byte[] getSignerPrivateKey(); + +} diff --git a/src/main/java/io/singularitynet/sdk/client/ConfigurationDependencyFactory.java b/src/main/java/io/singularitynet/sdk/client/ConfigurationDependencyFactory.java new file mode 100644 index 00000000..2aa83a79 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/client/ConfigurationDependencyFactory.java @@ -0,0 +1,51 @@ +package io.singularitynet.sdk.client; + +import java.net.URL; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.http.HttpService; +import org.web3j.tx.gas.ContractGasProvider; +import org.web3j.tx.gas.DefaultGasProvider; +import io.ipfs.api.IPFS; + +import io.singularitynet.sdk.ethereum.Signer; +import io.singularitynet.sdk.ethereum.MnemonicIdentity; +import io.singularitynet.sdk.ethereum.PrivateKeyIdentity; + +public class ConfigurationDependencyFactory implements DependencyFactory { + + private final Configuration config; + + public ConfigurationDependencyFactory(Configuration config) { + this.config = config; + } + + @Override + public Web3j getWeb3j() { + return Web3j.build(new HttpService(config.getEthereumJsonRpcEndpoint())); + } + + @Override + public ContractGasProvider getContractGasProvider(Web3j web3k) { + return new DefaultGasProvider(); + } + + @Override + public IPFS getIpfs() { + URL ipfsUrl = config.getIpfsUrl(); + // TODO: support https + return new IPFS(ipfsUrl.getHost(), ipfsUrl.getPort()); + } + + @Override + public Signer getSigner() { + switch (config.getSignerType()) { + case MNEMONIC: + return new MnemonicIdentity(config.getSignerMnemonic(), 0); + case PRIVATE_KEY: + return new PrivateKeyIdentity(config.getSignerPrivateKey()); + default: + throw new IllegalArgumentException("Unexpected signer type: " + config.getSignerType()); + } + } + +} diff --git a/src/main/java/io/singularitynet/sdk/client/DependencyFactory.java b/src/main/java/io/singularitynet/sdk/client/DependencyFactory.java new file mode 100644 index 00000000..80ec5e60 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/client/DependencyFactory.java @@ -0,0 +1,16 @@ +package io.singularitynet.sdk.client; + +import org.web3j.protocol.Web3j; +import org.web3j.tx.gas.ContractGasProvider; +import io.ipfs.api.IPFS; + +import io.singularitynet.sdk.ethereum.Signer; + +public interface DependencyFactory { + + Web3j getWeb3j(); + ContractGasProvider getContractGasProvider(Web3j web3j); + IPFS getIpfs(); + Signer getSigner(); + +} diff --git a/src/main/java/io/singularitynet/sdk/client/FixedPaymentChannelPaymentStrategy.java b/src/main/java/io/singularitynet/sdk/client/FixedPaymentChannelPaymentStrategy.java new file mode 100644 index 00000000..97bb367c --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/client/FixedPaymentChannelPaymentStrategy.java @@ -0,0 +1,57 @@ +package io.singularitynet.sdk.client; + +import java.math.BigInteger; +import java.util.Arrays; + +import io.singularitynet.sdk.common.Utils; +import io.singularitynet.sdk.daemon.Payment; +import io.singularitynet.sdk.mpe.PaymentChannel; +import io.singularitynet.sdk.mpe.EscrowPayment; +import io.singularitynet.sdk.registry.*; + +/** + * The class is responsible for providing a payment for the client call using + * the specified payment channel. + */ +public class FixedPaymentChannelPaymentStrategy implements PaymentStrategy { + + private final BigInteger channelId; + + /** + * Constructor. + * @param channelId id of the payment channel to use for the payment + * generation. + */ + public FixedPaymentChannelPaymentStrategy(BigInteger channelId) { + this.channelId = channelId; + } + + @Override + public Payment getPayment(GrpcCallParameters callParams, + ServiceClient serviceClient) { + PaymentChannel channel = serviceClient.getPaymentChannelProvider() + .getChannelById(channelId); + BigInteger price = getPrice(channel, serviceClient); + // TODO: test on price exceeds channel value + BigInteger newAmount = channel.getSpentAmount().add(price); + return EscrowPayment.newBuilder() + .setPaymentChannel(channel) + .setAmount(newAmount) + .setSigner(serviceClient.getSigner()) + .build(); + } + + private BigInteger getPrice(PaymentChannel channel, ServiceClient serviceClient) { + ServiceMetadata serviceMetadata = serviceClient.getMetadataProvider().getServiceMetadata(); + // TODO: this can contradict to failover strategy: + // how to align endpoint group selected by failover and payment group? + EndpointGroup group = serviceMetadata.getEndpointGroups().stream() + .filter(grp -> Arrays.equals(channel.getPaymentGroupId(), grp.getPaymentGroupId())) + .findFirst().get(); + Pricing price = group.getPricing().stream() + .filter(pr -> PriceModel.FIXED_PRICE.equals(pr.getPriceModel())) + .findFirst().get(); + return price.getPriceInCogs(); + } + +} diff --git a/src/main/java/io/singularitynet/sdk/client/GrpcCallParameters.java b/src/main/java/io/singularitynet/sdk/client/GrpcCallParameters.java new file mode 100644 index 00000000..55ed91ca --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/client/GrpcCallParameters.java @@ -0,0 +1,32 @@ +package io.singularitynet.sdk.client; + +import io.grpc.MethodDescriptor; +import io.grpc.CallOptions; +import io.grpc.Channel; + +public class GrpcCallParameters { + + private final MethodDescriptor method; + private final CallOptions callOptions; + private final Channel channel; + + public GrpcCallParameters(MethodDescriptor method, + CallOptions callOptions, Channel channel) { + this.method = method; + this.callOptions = callOptions; + this.channel = channel; + } + + public MethodDescriptor getMethod() { + return method; + } + + public CallOptions getCallOptions() { + return callOptions; + } + + public Channel getChannel() { + return channel; + } + +} diff --git a/src/main/java/io/singularitynet/sdk/client/JsonConfiguration.java b/src/main/java/io/singularitynet/sdk/client/JsonConfiguration.java new file mode 100644 index 00000000..6674adff --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/client/JsonConfiguration.java @@ -0,0 +1,59 @@ +package io.singularitynet.sdk.client; + +import java.net.URL; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.FieldNamingPolicy; +import com.google.common.base.Preconditions; + +import io.singularitynet.sdk.common.Utils; +import io.singularitynet.sdk.ethereum.Signer; +import io.singularitynet.sdk.ethereum.MnemonicIdentity; +import io.singularitynet.sdk.ethereum.PrivateKeyIdentity; + +public class JsonConfiguration implements Configuration { + + private final String ethereumJsonRpcEndpoint; + private final URL ipfsUrl; + private final String signerType; + private final String signerMnemonic; + private final String signerPrivateKeyBase64; + + public JsonConfiguration(String json) { + Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + JsonConfiguration config = gson.fromJson(json, JsonConfiguration.class); + Preconditions.checkArgument(config.ethereumJsonRpcEndpoint != null, + "Field ethereum_json_rpc_endpoint is required"); + this.ethereumJsonRpcEndpoint = config.ethereumJsonRpcEndpoint; + this.ipfsUrl = config.ipfsUrl; + this.signerType = config.signerType; + this.signerMnemonic = config.signerMnemonic; + this.signerPrivateKeyBase64 = config.signerPrivateKeyBase64; + } + + @Override + public String getEthereumJsonRpcEndpoint() { + return ethereumJsonRpcEndpoint; + } + + @Override + public URL getIpfsUrl() { + return ipfsUrl; + } + + @Override + public SignerType getSignerType() { + return Enum.valueOf(SignerType.class, signerType.toUpperCase()); + } + + @Override + public String getSignerMnemonic() { + return signerMnemonic; + } + + @Override + public byte[] getSignerPrivateKey() { + return Utils.base64ToBytes(signerPrivateKeyBase64); + } + +} diff --git a/src/main/java/io/singularitynet/sdk/client/PaymentStrategy.java b/src/main/java/io/singularitynet/sdk/client/PaymentStrategy.java new file mode 100644 index 00000000..9c1811aa --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/client/PaymentStrategy.java @@ -0,0 +1,21 @@ +package io.singularitynet.sdk.client; + +import io.singularitynet.sdk.daemon.Payment; + +/** + * The strategy provides a payment for the client call. + */ +public interface PaymentStrategy { + + /** + * Return the payment for the client call. + * @param type of the gRPC request of the call. + * @param type of the gRPC response of the call; + * @param parameters provides the information about the gRPC call context. + * @param serviceClient provides the information about the platform service + * context. + * @return instance of the Payment class. + */ + Payment getPayment(GrpcCallParameters parameters, ServiceClient serviceClient); + +} diff --git a/src/main/java/io/singularitynet/sdk/client/Sdk.java b/src/main/java/io/singularitynet/sdk/client/Sdk.java new file mode 100644 index 00000000..836c4af0 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/client/Sdk.java @@ -0,0 +1,111 @@ +package io.singularitynet.sdk.client; + +import org.web3j.tx.ReadonlyTransactionManager; +import org.web3j.protocol.Web3j; +import org.web3j.tx.gas.ContractGasProvider; +import io.ipfs.api.IPFS; +import com.google.gson.Gson; +import java.io.InputStreamReader; +import com.google.common.base.Preconditions; +import java.lang.reflect.Type; +import com.google.gson.reflect.TypeToken; +import java.util.Map; + +import io.singularitynet.sdk.contracts.Registry; +import io.singularitynet.sdk.contracts.MultiPartyEscrow; +import io.singularitynet.sdk.registry.RegistryContract; +import io.singularitynet.sdk.registry.MetadataStorage; +import io.singularitynet.sdk.registry.IpfsMetadataStorage; +import io.singularitynet.sdk.registry.MetadataProvider; +import io.singularitynet.sdk.registry.RegistryMetadataProvider; +import io.singularitynet.sdk.daemon.DaemonConnection; +import io.singularitynet.sdk.daemon.FirstEndpointDaemonConnection; +import io.singularitynet.sdk.daemon.PaymentChannelStateService; +import io.singularitynet.sdk.mpe.MultiPartyEscrowContract; +import io.singularitynet.sdk.mpe.PaymentChannelProvider; +import io.singularitynet.sdk.mpe.AskDaemonFirstPaymentChannelProvider; +import io.singularitynet.sdk.client.PaymentStrategy; +import io.singularitynet.sdk.client.ServiceClient; +import io.singularitynet.sdk.client.BaseServiceClient; +import io.singularitynet.sdk.common.Utils; +import io.singularitynet.sdk.ethereum.Address; +import io.singularitynet.sdk.ethereum.Signer; + +public class Sdk { + + private final Web3j web3j; + private final IPFS ipfs; + private final ContractGasProvider gasProvider; + private final Signer signer; + + public Sdk(Configuration config) { + this(new ConfigurationDependencyFactory(config)); + } + + public Sdk(DependencyFactory factory) { + this.web3j = factory.getWeb3j(); + this.ipfs = factory.getIpfs(); + this.gasProvider = factory.getContractGasProvider(web3j); + this.signer = factory.getSigner(); + } + + public ServiceClient newServiceClient(String orgId, String serviceId, + String endpointGroupName, PaymentStrategy paymentStrategy) { + + String networkId = Utils.wrapExceptions(() -> { + return web3j.netVersion().send().getNetVersion(); + }); + ReadonlyTransactionManager transactionManager = new ReadonlyTransactionManager( + // TODO: add unit test on prefix adding + web3j, signer.getAddress().toString()); + + Address registryAddress = readContractAddress(networkId, "networks/Registry.json", "Registry"); + Registry registry = Registry.load(registryAddress.toString(), web3j, + transactionManager, gasProvider); + RegistryContract registryContract = new RegistryContract(registry); + MetadataStorage metadataStorage = new IpfsMetadataStorage(ipfs); + MetadataProvider metadataProvider = new RegistryMetadataProvider( + orgId, serviceId, registryContract, metadataStorage); + + Address mpeAddress = readContractAddress(networkId, "networks/MultiPartyEscrow.json", "MultiPartyEscrow"); + MultiPartyEscrow mpe = MultiPartyEscrow.load(mpeAddress.toString(), web3j, + transactionManager, gasProvider); + MultiPartyEscrowContract mpeContract = new MultiPartyEscrowContract(mpe); + + DaemonConnection connection = new FirstEndpointDaemonConnection( + endpointGroupName, metadataProvider); + PaymentChannelStateService stateService = new PaymentChannelStateService( + connection, mpeContract, web3j, signer); + PaymentChannelProvider paymentChannelProvider = + new AskDaemonFirstPaymentChannelProvider(mpeContract, stateService); + + return new BaseServiceClient(connection, metadataProvider, + paymentChannelProvider, paymentStrategy, signer); + } + + public void shutdown() { + web3j.shutdown(); + } + + private static Address readContractAddress(String networkId, String networkJson, String contractName) { + return Utils.wrapExceptions(() -> { + InputStreamReader jsonReader = new InputStreamReader(Sdk.class.getClassLoader().getResourceAsStream(networkJson)); + try { + Gson gson = new Gson(); + Type jsonType = new TypeToken>>(){}.getType(); + Map> networks = gson.fromJson(jsonReader, jsonType); + Map network = networks.get(networkId); + // TODO: test precondition + Preconditions.checkState(network != null, "No configuration for network %s found", networkId); + Object address = network.get("address"); + // TODO: test precondition + Preconditions.checkState(address != null, "No address of %s contract found in the network %s configuration", + contractName, networkId); + return new Address((String) address); + } finally { + jsonReader.close(); + } + }); + } + +} diff --git a/src/main/java/io/singularitynet/sdk/client/ServiceClient.java b/src/main/java/io/singularitynet/sdk/client/ServiceClient.java index 11bfb6f7..cec51ccb 100644 --- a/src/main/java/io/singularitynet/sdk/client/ServiceClient.java +++ b/src/main/java/io/singularitynet/sdk/client/ServiceClient.java @@ -3,9 +3,47 @@ import io.grpc.Channel; import java.util.function.Function; +import io.singularitynet.sdk.mpe.PaymentChannelProvider; +import io.singularitynet.sdk.registry.MetadataProvider; +import io.singularitynet.sdk.ethereum.Signer; + +/** + * The interface provides all necessary facilities to work with the platform + * service. + */ public interface ServiceClient { + /** + * Return an instance of the metadata provider. + * @return metadata provider instance. + */ + MetadataProvider getMetadataProvider(); + + /** + * Return an instance of the payment channel provider. + * @return payment channel provider instance. + */ + PaymentChannelProvider getPaymentChannelProvider(); + + /** + * Return the signer to sign payments. + */ + Signer getSigner(); + + /** + * Construct new gRPC stub to call the platform service. + * @param type of the gRPC service stub. + * @param constructor constructs new gRPC stub from the passed gRPC + * channel. + * @return gRPC stub constracted. + */ T getGrpcStub(Function constructor); + + /** + * Closes platform service connection. This call causes calling + * DaemonConnection.shutdownNow(). + */ void shutdownNow(); + } diff --git a/src/main/java/io/singularitynet/sdk/common/Utils.java b/src/main/java/io/singularitynet/sdk/common/Utils.java new file mode 100644 index 00000000..99bfd2d5 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/common/Utils.java @@ -0,0 +1,103 @@ +package io.singularitynet.sdk.common; + +import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.base.Preconditions; +import java.util.concurrent.Callable; +import java.util.Base64; +import java.math.BigInteger; +import java.util.Arrays; + +public class Utils { + + private Utils() { + } + + public static byte[] strToBytes(String str) { + return str.getBytes(UTF_8); + } + + public static String bytesToStr(byte[] bytes) { + String str = new String(bytes, UTF_8); + int zeroPos = str.indexOf(0); + if (zeroPos == -1) { + return str; + } + return str.substring(0, zeroPos); + } + + public static byte[] strToBytes32(String str) { + Preconditions.checkArgument(str.length() <= 32, "Passed string length exceeds 32 bytes"); + byte[] bytes32 = new byte[32]; + int i = 0; + for (byte b : str.getBytes(UTF_8)) { + bytes32[i++] = b; + } + return bytes32; + } + + public static String bytes32ToStr(byte[] bytes) { + Preconditions.checkArgument(bytes.length == 32, "Passed array length is not equal to 32 bytes"); + String full = new String(bytes, UTF_8); + int lastZero = full.indexOf(0); + if (lastZero == -1) { + return full; + } + return full.substring(0, lastZero); + } + + public static byte[] base64ToBytes(String str) { + return Base64.getDecoder().decode(str); + } + + public static String bytesToBase64(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + public static byte[] bigIntToBytes32(BigInteger value) { + byte[] bytes32 = new byte[32]; + byte[] bytes = value.toByteArray(); + // TODO: check bytes length is not greater than 32 + System.arraycopy(bytes, 0, bytes32, 32 - bytes.length, bytes.length); + return bytes32; + } + + public static BigInteger bytes32ToBigInt(byte[] bytes) { + // TODO: check bytes length is equal to 32 + return new BigInteger(bytes); + } + + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + public static String bytesToHex(byte[] bytes) { + StringBuffer hex = new StringBuffer(); + int eight = 0; + for (byte b : bytes) { + hex.append(HEX_ARRAY[(b >> 4) & 0xF]).append(HEX_ARRAY[b & 0xF]).append(" "); + eight++; + if (eight == 8) { + hex.append(" "); + eight = 0; + } + } + return hex.toString(); + } + + public static byte[] hexToBytes(String str) { + Preconditions.checkArgument(str.length() % 2 == 0, "String should contain even number of hex digits"); + byte[] bytes = new byte[str.length() / 2]; + for (int i = 0; i < str.length(); i += 2) { + bytes[i / 2] = (byte) ((Character.digit(str.charAt(i), 16) << 4) + + Character.digit(str.charAt(i + 1), 16)); + } + return bytes; + } + + public static T wrapExceptions(Callable callable) { + try { + return callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/io/singularitynet/sdk/daemon/DaemonConnection.java b/src/main/java/io/singularitynet/sdk/daemon/DaemonConnection.java new file mode 100644 index 00000000..24d6c728 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/daemon/DaemonConnection.java @@ -0,0 +1,31 @@ +package io.singularitynet.sdk.daemon; + +import java.util.function.Function; +import io.grpc.Channel; +import io.grpc.ClientInterceptor; + +public interface DaemonConnection { + + /** + * Construct new gRPC stub to call the platform service. + * @param type of the gRPC service stub. + * @param constructor constructs new gRPC stub from the passed gRPC + * channel. + * @return gRPC stub constracted. + */ + T getGrpcStub(Function constructor); + + /** + * Set gRPC interceptor which is called before each service call. + * Main purpose of the interceptor is to provide payment for the call. + * @param interceptor interceptor instance. + */ + void setClientCallsInterceptor(ClientInterceptor interceptor); + + /** + * Closes platform service connection. This call causes calling + * shutdownNow() on each stub returned by getGrpcStub() method. + */ + void shutdownNow(); + +} diff --git a/src/main/java/io/singularitynet/sdk/daemon/FirstEndpointDaemonConnection.java b/src/main/java/io/singularitynet/sdk/daemon/FirstEndpointDaemonConnection.java new file mode 100644 index 00000000..8cb61a67 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/daemon/FirstEndpointDaemonConnection.java @@ -0,0 +1,92 @@ +package io.singularitynet.sdk.daemon; + +import io.grpc.*; +import java.net.URL; +import java.util.function.Function; + +import io.singularitynet.sdk.registry.MetadataProvider; +import io.singularitynet.sdk.registry.ServiceMetadata; + +public class FirstEndpointDaemonConnection implements DaemonConnection { + + private final String groupName; + private final MetadataProvider metadataProvider; + private final ClientInterceptorProxy interceptorProxy; + + private ManagedChannel channel; + + public FirstEndpointDaemonConnection(String groupName, MetadataProvider metadataProvider) { + this.groupName = groupName; + this.metadataProvider = metadataProvider; + this.interceptorProxy = new ClientInterceptorProxy(); + } + + @Override + public T getGrpcStub(Function constructor) { + return constructor.apply(getChannelLazy()); + } + + @Override + public void setClientCallsInterceptor(ClientInterceptor interceptor) { + interceptorProxy.setDelegate(interceptor); + } + + @Override + public void shutdownNow() { + channel.shutdownNow(); + } + + private ManagedChannel getChannelLazy() { + // TODO: make thread safe + if (channel == null) { + channel = getChannel(); + } + return channel; + } + + private ManagedChannel getChannel() { + ServiceMetadata serviceMetadata = metadataProvider.getServiceMetadata(); + URL url = serviceMetadata.getEndpointGroups().stream() + .filter(group -> groupName.equals(group.getGroupName())) + .findFirst().get().getEndpoints().get(0); + ManagedChannelBuilder builder = ManagedChannelBuilder + .forAddress(url.getHost(), url.getPort()) + .intercept(interceptorProxy); + // TODO: test HTTPS connections + if ("http".equals(url.getProtocol())) { + builder.usePlaintext(); + } + return builder.build(); + } + + // ThreadSafe + private static class ClientInterceptorProxy implements ClientInterceptor { + + private volatile ClientInterceptor delegate; + + public void setDelegate(ClientInterceptor delegate) { + this.delegate = delegate; + } + + private static final String PAYMENT_CHANNEL_STATE_SERVICE = "escrow.PaymentChannelStateService"; + + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + + if (PAYMENT_CHANNEL_STATE_SERVICE.equals(getServiceName(method))) { + return next.newCall(method, callOptions); + } + + return delegate.interceptCall(method, callOptions, next); + } + + } + + private static String getServiceName(MethodDescriptor method) { + return MethodDescriptor.extractFullServiceName(method.getFullMethodName()); + } + +} diff --git a/src/main/java/io/singularitynet/sdk/daemon/Payment.java b/src/main/java/io/singularitynet/sdk/daemon/Payment.java new file mode 100644 index 00000000..a1a0a220 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/daemon/Payment.java @@ -0,0 +1,11 @@ +package io.singularitynet.sdk.daemon; + +import io.grpc.Metadata; + +public interface Payment { + + static final Metadata.Key SNET_PAYMENT_TYPE = Metadata.Key.of("snet-payment-type", Metadata.ASCII_STRING_MARSHALLER); + + void toMetadata(Metadata headers); + +} diff --git a/src/main/java/io/singularitynet/sdk/daemon/PaymentChannelStateReply.java b/src/main/java/io/singularitynet/sdk/daemon/PaymentChannelStateReply.java new file mode 100644 index 00000000..3c7d5414 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/daemon/PaymentChannelStateReply.java @@ -0,0 +1,106 @@ +package io.singularitynet.sdk.daemon; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import java.math.BigInteger; + +@EqualsAndHashCode +@ToString +public class PaymentChannelStateReply { + + private final BigInteger currentNonce; + private final BigInteger currentSignedAmount; + // TODO: replace by Signature class to implement toString() + private final byte[] currentSignature; + private final BigInteger oldNonceSignedAmount; + private final byte[] oldNonceSignature; + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder(this); + } + + private PaymentChannelStateReply(Builder builder) { + this.currentNonce = builder.currentNonce; + this.currentSignedAmount = builder.currentSignedAmount; + this.currentSignature = builder.currentSignature; + this.oldNonceSignedAmount = builder.oldNonceSignedAmount; + this.oldNonceSignature = builder.oldNonceSignature; + } + + public BigInteger getCurrentNonce() { + return currentNonce; + } + + public BigInteger getCurrentSignedAmount() { + return currentSignedAmount; + } + + public boolean hasCurrentSignedAmount() { + return currentSignedAmount != null; + } + + public byte[] getCurrentSignature() { + return currentSignature; + } + + public BigInteger getOldNonceSignedAmount() { + return oldNonceSignedAmount; + } + + public byte[] getOldNonceSignature() { + return oldNonceSignature; + } + + public static class Builder { + + private BigInteger currentNonce; + private BigInteger currentSignedAmount; + private byte[] currentSignature; + private BigInteger oldNonceSignedAmount; + private byte[] oldNonceSignature; + + private Builder() { + } + + private Builder(PaymentChannelStateReply object) { + this.currentNonce = object.currentNonce; + this.currentSignedAmount = object.currentSignedAmount; + this.currentSignature = object.currentSignature; + this.oldNonceSignedAmount = object.oldNonceSignedAmount; + this.oldNonceSignature = object.oldNonceSignature; + } + + public Builder setCurrentNonce(BigInteger currentNonce) { + this.currentNonce = currentNonce; + return this; + } + + public Builder setCurrentSignedAmount(BigInteger currentSignedAmount) { + this.currentSignedAmount = currentSignedAmount; + return this; + } + + public Builder setCurrentSignature(byte[] currentSignature) { + this.currentSignature = currentSignature; + return this; + } + + public Builder setOldNonceSignedAmount(BigInteger oldNonceSignedAmount) { + this.oldNonceSignedAmount = oldNonceSignedAmount; + return this; + } + + public Builder setOldNonceSignature(byte[] oldNonceSignature) { + this.oldNonceSignature = oldNonceSignature; + return this; + } + + public PaymentChannelStateReply build() { + return new PaymentChannelStateReply(this); + } + } +} diff --git a/src/main/java/io/singularitynet/sdk/daemon/PaymentChannelStateService.java b/src/main/java/io/singularitynet/sdk/daemon/PaymentChannelStateService.java new file mode 100644 index 00000000..14b3d6da --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/daemon/PaymentChannelStateService.java @@ -0,0 +1,90 @@ +package io.singularitynet.sdk.daemon; + +import java.math.BigInteger; +import java.io.ByteArrayOutputStream; +import com.google.protobuf.ByteString; +import org.web3j.protocol.core.Ethereum; +import org.web3j.protocol.core.methods.response.EthBlockNumber; + +import io.singularitynet.daemon.escrow.*; +import io.singularitynet.daemon.escrow.StateService.*; +import io.singularitynet.daemon.escrow.PaymentChannelStateServiceGrpc.*; +import io.singularitynet.sdk.common.Utils; +import io.singularitynet.sdk.ethereum.Signer; +import io.singularitynet.sdk.ethereum.Address; +import io.singularitynet.sdk.mpe.MultiPartyEscrowContract; + +public class PaymentChannelStateService { + + private final MessageSigningHelper signingHelper; + private final PaymentChannelStateServiceBlockingStub stub; + + public PaymentChannelStateService(DaemonConnection daemonConnection, + MultiPartyEscrowContract mpe, Ethereum ethereum, Signer signer) { + this.signingHelper = new MessageSigningHelper(mpe.getContractAddress(), ethereum, signer); + this.stub = daemonConnection.getGrpcStub(PaymentChannelStateServiceGrpc::newBlockingStub); + } + + public PaymentChannelStateReply getChannelState(BigInteger channelId) { + ChannelStateRequest.Builder request = ChannelStateRequest.newBuilder() + .setChannelId(toBytesString(channelId)); + + signingHelper.signChannelStateRequest(request); + + ChannelStateReply grpcReply = stub.getChannelState(request.build()); + + PaymentChannelStateReply.Builder reply = PaymentChannelStateReply.newBuilder() + .setCurrentNonce(toBigInt(grpcReply.getCurrentNonce())); + + if (grpcReply.getCurrentSignedAmount() == ByteString.EMPTY) { + return reply.build(); + } + + reply.setCurrentSignedAmount(toBigInt(grpcReply.getCurrentSignedAmount())); + reply.setCurrentSignature(grpcReply.getCurrentSignature().toByteArray()); + + return reply.build(); + } + + private static ByteString toBytesString(BigInteger value) { + return ByteString.copyFrom(Utils.bigIntToBytes32(value)); + } + + private static BigInteger toBigInt(ByteString value) { + return Utils.bytes32ToBigInt(value.toByteArray()); + } + + static class MessageSigningHelper { + + private static final byte[] GET_CHANNEL_STATE_PREFIX = Utils.strToBytes("__get_channel_state"); + + private final byte[] mpeContractAddress; + private final Ethereum ethereum; + private final Signer signer; + + public MessageSigningHelper(Address mpeAddress, Ethereum ethereum, + Signer signer) { + this.mpeContractAddress = mpeAddress.toByteArray(); + this.ethereum = ethereum; + this.signer = signer; + } + + public void signChannelStateRequest(ChannelStateRequest.Builder request) { + Utils.wrapExceptions(() -> { + long block = ethereum.ethBlockNumber().send().getBlockNumber().longValue(); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(GET_CHANNEL_STATE_PREFIX); + bytes.write(mpeContractAddress); + bytes.write(request.getChannelId().toByteArray()); + bytes.write(Utils.bigIntToBytes32(BigInteger.valueOf(block))); + + return request + .setCurrentBlock(block) + .setSignature(ByteString.copyFrom(signer.sign(bytes.toByteArray()))); + }); + } + + } + +} diff --git a/src/main/java/io/singularitynet/sdk/daemon/PaymentSerializer.java b/src/main/java/io/singularitynet/sdk/daemon/PaymentSerializer.java new file mode 100644 index 00000000..f58e312d --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/daemon/PaymentSerializer.java @@ -0,0 +1,52 @@ +package io.singularitynet.sdk.daemon; + +import io.grpc.Metadata; +import java.util.Optional; +import java.util.Map; +import java.util.HashMap; +import java.math.BigInteger; +import java.util.function.Function; + +public class PaymentSerializer { + + private static final Map> readerByType = new HashMap<>(); + + public static void register(String type, Function reader) { + readerByType.put(type, reader); + } + + public static Optional fromMetadata(Metadata headers) { + if (!headers.containsKey(Payment.SNET_PAYMENT_TYPE)) { + return Optional.empty(); + } + + String paymentType = headers.get(Payment.SNET_PAYMENT_TYPE); + Function reader = readerByType.get(paymentType); + + if (reader == null) { + throw new IllegalArgumentException("Unexpected payment type: " + paymentType); + } + + return Optional.of(reader.apply(headers)); + } + + public static void toMetadata(Payment payment, Metadata headers) { + payment.toMetadata(headers); + } + + public static final Metadata.AsciiMarshaller ASCII_BIGINTEGER_MARSHALLER = + new Metadata.AsciiMarshaller() { + + @Override + public BigInteger parseAsciiString(String serialized) { + return new BigInteger(serialized); + } + + @Override + public String toAsciiString(BigInteger value) { + return value.toString(); + } + + }; + +} diff --git a/src/main/java/io/singularitynet/sdk/ethereum/Address.java b/src/main/java/io/singularitynet/sdk/ethereum/Address.java new file mode 100644 index 00000000..be2030a7 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/ethereum/Address.java @@ -0,0 +1,32 @@ +package io.singularitynet.sdk.ethereum; + +import lombok.EqualsAndHashCode; +import com.google.common.base.Preconditions; + +import io.singularitynet.sdk.common.Utils; + +@EqualsAndHashCode +public class Address { + + private final String address; + + public Address(String address) { + if (address.startsWith("0x")) { + Preconditions.checkArgument(address.length() == 42, "Address length is not equal to 40: %s", address); + this.address = address.substring(2); + } else { + Preconditions.checkArgument(address.length() == 40, "Address length is not equal to 40: %s", address); + this.address = address; + } + } + + @Override + public String toString() { + return "0x" + address; + } + + public byte[] toByteArray() { + return Utils.hexToBytes(address); + } + +} diff --git a/src/main/java/io/singularitynet/sdk/ethereum/CryptoUtils.java b/src/main/java/io/singularitynet/sdk/ethereum/CryptoUtils.java new file mode 100644 index 00000000..17532eb0 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/ethereum/CryptoUtils.java @@ -0,0 +1,38 @@ +package io.singularitynet.sdk.ethereum; + +import static com.google.common.base.Preconditions.checkState; +import java.io.ByteArrayInputStream; +import java.math.BigInteger; +import org.web3j.crypto.Keys; +import org.web3j.crypto.Sign; +import org.web3j.crypto.Hash; + +import io.singularitynet.sdk.common.Utils; + +public class CryptoUtils { + + private CryptoUtils() { + } + + public static Address getSignerAddress(byte[] message, byte[] signature) { + return Utils.wrapExceptions(() -> { + Sign.SignatureData signatureData = bytesToSignature(signature); + BigInteger publicKey = Sign.signedPrefixedMessageToKey(Hash.sha3(message), signatureData); + return new Address(Keys.getAddress(publicKey)); + }); + } + + private static Sign.SignatureData bytesToSignature(byte[] signature) { + checkState(signature.length == 65, "Incorrect signature length: not equal to 65"); + byte[] r = new byte[32]; + byte[] s = new byte[32]; + byte[] v = new byte[1]; + return Utils.wrapExceptions(() -> { + ByteArrayInputStream bytes = new ByteArrayInputStream(signature); + bytes.read(r, 0, 32); + bytes.read(s, 0, 32); + bytes.read(v, 0, 1); + return new Sign.SignatureData(v, r, s); + }); + } +} diff --git a/src/main/java/io/singularitynet/sdk/ethereum/MnemonicIdentity.java b/src/main/java/io/singularitynet/sdk/ethereum/MnemonicIdentity.java new file mode 100644 index 00000000..20d322d8 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/ethereum/MnemonicIdentity.java @@ -0,0 +1,29 @@ +package io.singularitynet.sdk.ethereum; + +import java.util.Arrays; + +import org.web3j.crypto.MnemonicUtils; +import org.web3j.crypto.Bip32ECKeyPair; + +public class MnemonicIdentity extends PrivateKeyIdentity { + + private static final int[] PATH_PREFIX = new int[] { + 44 | Bip32ECKeyPair.HARDENED_BIT, + 60 | Bip32ECKeyPair.HARDENED_BIT, + 0 | Bip32ECKeyPair.HARDENED_BIT, + 0 + }; + + public MnemonicIdentity(String mnemonic, int walletIndex) { + super(wallet(mnemonic, walletIndex)); + } + + private static Bip32ECKeyPair wallet(String mnemonic, int walletIndex) { + byte[] seed = MnemonicUtils.generateSeed(mnemonic, ""); + Bip32ECKeyPair master = Bip32ECKeyPair.generateKeyPair(seed); + int[] path = Arrays.copyOf(PATH_PREFIX, PATH_PREFIX.length + 1); + path[path.length - 1] = walletIndex; + return Bip32ECKeyPair.deriveKeyPair(master, path); + } + +} diff --git a/src/main/java/io/singularitynet/sdk/ethereum/PrivateKeyIdentity.java b/src/main/java/io/singularitynet/sdk/ethereum/PrivateKeyIdentity.java new file mode 100644 index 00000000..9d8f59bd --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/ethereum/PrivateKeyIdentity.java @@ -0,0 +1,49 @@ +package io.singularitynet.sdk.ethereum; + +import java.math.BigInteger; +import java.io.ByteArrayOutputStream; +import org.web3j.crypto.ECKeyPair; +import org.web3j.crypto.Sign; +import org.web3j.crypto.Keys; +import org.web3j.crypto.Hash; +import static com.google.common.base.Preconditions.checkState; + +import io.singularitynet.sdk.common.Utils; + +public class PrivateKeyIdentity implements Signer { + + private final ECKeyPair key; + + public PrivateKeyIdentity(ECKeyPair key) { + this.key = key; + } + + public PrivateKeyIdentity(BigInteger privateKey) { + this(ECKeyPair.create(privateKey)); + } + + public PrivateKeyIdentity(byte[] privateKey) { + this(ECKeyPair.create(privateKey)); + } + + @Override + public byte[] sign(byte[] message) { + return signatureToBytes(Sign.signPrefixedMessage(Hash.sha3(message), key)); + } + + @Override + public Address getAddress() { + return new Address(Keys.getAddress(key.getPublicKey())); + } + + private static byte[] signatureToBytes(Sign.SignatureData signature) { + return Utils.wrapExceptions(() -> { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(signature.getR()); + bytes.write(signature.getS()); + bytes.write(signature.getV()); + return bytes.toByteArray(); + }); + } + +} diff --git a/src/main/java/io/singularitynet/sdk/ethereum/Signer.java b/src/main/java/io/singularitynet/sdk/ethereum/Signer.java new file mode 100644 index 00000000..22f4678b --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/ethereum/Signer.java @@ -0,0 +1,7 @@ +package io.singularitynet.sdk.ethereum; + +public interface Signer extends WithAddress { + + byte[] sign(byte[] message); + +} diff --git a/src/main/java/io/singularitynet/sdk/ethereum/WithAddress.java b/src/main/java/io/singularitynet/sdk/ethereum/WithAddress.java new file mode 100644 index 00000000..5f57c4a3 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/ethereum/WithAddress.java @@ -0,0 +1,7 @@ +package io.singularitynet.sdk.ethereum; + +public interface WithAddress { + + Address getAddress(); + +} diff --git a/src/main/java/io/singularitynet/sdk/mpe/AskDaemonFirstPaymentChannelProvider.java b/src/main/java/io/singularitynet/sdk/mpe/AskDaemonFirstPaymentChannelProvider.java new file mode 100644 index 00000000..3300a04f --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/mpe/AskDaemonFirstPaymentChannelProvider.java @@ -0,0 +1,80 @@ +package io.singularitynet.sdk.mpe; + +import java.math.BigInteger; +import static com.google.common.base.Preconditions.checkState; + +import io.singularitynet.sdk.daemon.PaymentChannelStateReply; +import io.singularitynet.sdk.daemon.PaymentChannelStateService; +import io.singularitynet.sdk.ethereum.CryptoUtils; +import io.singularitynet.sdk.ethereum.Address; + +public class AskDaemonFirstPaymentChannelProvider implements PaymentChannelProvider { + + private final MultiPartyEscrowContract mpe; + private final PaymentChannelStateService stateService; + + public AskDaemonFirstPaymentChannelProvider(MultiPartyEscrowContract mpe, + PaymentChannelStateService stateService) { + this.mpe = mpe; + this.stateService = stateService; + } + + @Override + public PaymentChannel getChannelById(BigInteger channelId) { + PaymentChannel channel = mpe.getChannelById(channelId).get(); + PaymentChannelStateReply reply = stateService.getChannelState(channelId); + if (!reply.hasCurrentSignedAmount()) { + checkState(channel.getNonce().compareTo(reply.getCurrentNonce()) >= 0, + "Daemon sent channel state which is newer then blockchain one. " + + "Channel id: %s", channel.getChannelId()); + return channel; + } + return mergeChannelState(channel, reply); + } + + private static final BigInteger ONE = BigInteger.valueOf(1); + + private static PaymentChannel mergeChannelState(PaymentChannel channel, + PaymentChannelStateReply daemonState) { + + BigInteger spentAmount; + + if (channel.getNonce().equals(daemonState.getCurrentNonce())) { + verifySignature(channel, daemonState.getCurrentSignedAmount(), + daemonState.getCurrentSignature(), "last current nonce"); + spentAmount = daemonState.getCurrentSignedAmount(); + } else { + // TODO: test this case + checkState(daemonState.getCurrentNonce().subtract(channel.getNonce()) + .equals(ONE), "Difference between current channel nonce " + + "and daemon channel nonce is bigger than 1. Channel id: %s", + channel.getChannelId()); + verifySignature(channel, daemonState.getOldNonceSignedAmount(), + daemonState.getOldNonceSignature(), "last old nonce"); + verifySignature(channel.toBuilder().setNonce(daemonState.getCurrentNonce()).build(), + daemonState.getCurrentSignedAmount(), + daemonState.getCurrentSignature(), "last current nonce"); + spentAmount = daemonState.getCurrentSignedAmount().add( + daemonState.getOldNonceSignedAmount()); + } + + return channel.toBuilder() + .setSpentAmount(daemonState.getCurrentSignedAmount()) + .build(); + } + + private static void verifySignature(PaymentChannel channel, BigInteger amount, + byte[] signature, String type) { + byte[] payment = EscrowPayment.newBuilder() + .setPaymentChannel(channel) + .setAmount(amount) + .getMessage(); + Address address = CryptoUtils.getSignerAddress(payment, signature); + checkState(channel.getSigner().equals(address) || + channel.getSender().equals(address), + "Signature signer is not sender not signer. " + + "Daemon returned incorrect signature of the %s payment. " + + "Channel: %s, Payment signer: %s", type, channel, address); + } + +} diff --git a/src/main/java/io/singularitynet/sdk/mpe/EscrowPayment.java b/src/main/java/io/singularitynet/sdk/mpe/EscrowPayment.java new file mode 100644 index 00000000..9e7515e7 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/mpe/EscrowPayment.java @@ -0,0 +1,130 @@ +package io.singularitynet.sdk.mpe; + +import io.grpc.Metadata; +import java.math.BigInteger; +import java.io.ByteArrayOutputStream; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import io.singularitynet.sdk.daemon.Payment; +import io.singularitynet.sdk.daemon.PaymentSerializer; +import io.singularitynet.sdk.ethereum.Signer; +import io.singularitynet.sdk.common.Utils; + +@EqualsAndHashCode +@ToString +public class EscrowPayment implements Payment { + + public static final String PAYMENT_TYPE_ESCROW = "escrow"; + + static { + PaymentSerializer.register(PAYMENT_TYPE_ESCROW, EscrowPayment::fromMetadata); + } + + private static final Metadata.Key SNET_PAYMENT_CHANNEL_ID = + Metadata.Key.of("snet-payment-channel-id", PaymentSerializer.ASCII_BIGINTEGER_MARSHALLER); + private static final Metadata.Key SNET_PAYMENT_CHANNEL_NONCE = + Metadata.Key.of("snet-payment-channel-nonce", PaymentSerializer.ASCII_BIGINTEGER_MARSHALLER); + private static final Metadata.Key SNET_PAYMENT_CHANNEL_AMOUNT = + Metadata.Key.of("snet-payment-channel-amount", PaymentSerializer.ASCII_BIGINTEGER_MARSHALLER); + private static final Metadata.Key SNET_PAYMENT_CHANNEL_SIGNATURE = + Metadata.Key.of("snet-payment-channel-signature" + Metadata.BINARY_HEADER_SUFFIX, Metadata.BINARY_BYTE_MARSHALLER); + + private final BigInteger channelId; + private final BigInteger channelNonce; + private final BigInteger amount; + // TODO: replace by Signature class to implement toString() + private final byte[] signature; + + @Override + public void toMetadata(Metadata headers) { + headers.put(Payment.SNET_PAYMENT_TYPE, EscrowPayment.PAYMENT_TYPE_ESCROW); + headers.put(SNET_PAYMENT_CHANNEL_ID, channelId); + headers.put(SNET_PAYMENT_CHANNEL_NONCE, channelNonce); + headers.put(SNET_PAYMENT_CHANNEL_AMOUNT, amount); + headers.put(SNET_PAYMENT_CHANNEL_SIGNATURE, signature); + } + + public static EscrowPayment fromMetadata(Metadata headers) { + BigInteger channelId = headers.get(SNET_PAYMENT_CHANNEL_ID); + BigInteger channelNonce = headers.get(SNET_PAYMENT_CHANNEL_NONCE); + BigInteger amount = headers.get(SNET_PAYMENT_CHANNEL_AMOUNT); + byte[] signature = headers.get(SNET_PAYMENT_CHANNEL_SIGNATURE); + return new EscrowPayment(channelId, channelNonce, amount, signature); + } + + public EscrowPayment(BigInteger channelId, BigInteger channelNonce, + BigInteger amount, byte[] signature) { + this.channelId = channelId; + this.channelNonce = channelNonce; + this.amount = amount; + this.signature = signature; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public BigInteger getChannelId() { + return channelId; + } + + public BigInteger getChannelNonce() { + return channelNonce; + } + + public BigInteger getAmount() { + return amount; + } + + public byte[] getSignature() { + return signature; + } + + public static class Builder { + + private PaymentChannel paymentChannel; + private BigInteger amount; + private Signer signer; + + public Builder() { + } + + public Builder setPaymentChannel(PaymentChannel paymentChannel) { + this.paymentChannel = paymentChannel; + return this; + } + + public Builder setAmount(BigInteger amount) { + this.amount = amount; + return this; + } + + public Builder setSigner(Signer signer) { + this.signer = signer; + return this; + } + + public EscrowPayment build() { + byte[] signature = signer.sign(getMessage()); + return new EscrowPayment(paymentChannel.getChannelId(), + paymentChannel.getNonce(), amount, signature); + } + + private static final byte[] PAYMENT_MESSAGE_PREFIX = Utils.strToBytes("__MPE_claim_message"); + + byte[] getMessage() { + return Utils.wrapExceptions(() -> { + ByteArrayOutputStream message = new ByteArrayOutputStream(); + message.write(PAYMENT_MESSAGE_PREFIX); + message.write(paymentChannel.getMpeContractAddress().toByteArray()); + message.write(Utils.bigIntToBytes32(paymentChannel.getChannelId())); + message.write(Utils.bigIntToBytes32(paymentChannel.getNonce())); + message.write(Utils.bigIntToBytes32(amount)); + return message.toByteArray(); + }); + } + + } + +} diff --git a/src/main/java/io/singularitynet/sdk/mpe/MultiPartyEscrowContract.java b/src/main/java/io/singularitynet/sdk/mpe/MultiPartyEscrowContract.java new file mode 100644 index 00000000..cf21931f --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/mpe/MultiPartyEscrowContract.java @@ -0,0 +1,43 @@ +package io.singularitynet.sdk.mpe; + +import java.util.Optional; +import java.math.BigInteger; +import org.web3j.tuples.generated.Tuple7; + +import io.singularitynet.sdk.contracts.MultiPartyEscrow; +import io.singularitynet.sdk.common.Utils; +import io.singularitynet.sdk.ethereum.Address; + +public class MultiPartyEscrowContract { + + private final MultiPartyEscrow mpe; + + public MultiPartyEscrowContract(MultiPartyEscrow mpe) { + this.mpe = mpe; + } + + public Optional getChannelById(BigInteger channelId) { + return Utils.wrapExceptions(() -> { + // TODO: test what contract returns on non-existing channel id + Tuple7 channel = + mpe.channels(channelId).send(); + return Optional.of(PaymentChannel.newBuilder() + .setChannelId(channelId) + .setMpeContractAddress(getContractAddress()) + .setNonce(channel.component1()) + .setSender(new Address(channel.component2())) + .setSigner(new Address(channel.component3())) + .setRecipient(new Address(channel.component4())) + .setPaymentGroupId(channel.component5()) + .setValue(channel.component6()) + .setExpiration(channel.component7()) + .setSpentAmount(BigInteger.ZERO) + .build()); + }); + } + + public Address getContractAddress() { + return new Address(mpe.getContractAddress()); + } + +} diff --git a/src/main/java/io/singularitynet/sdk/mpe/PaymentChannel.java b/src/main/java/io/singularitynet/sdk/mpe/PaymentChannel.java new file mode 100644 index 00000000..f414bd4c --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/mpe/PaymentChannel.java @@ -0,0 +1,170 @@ +package io.singularitynet.sdk.mpe; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import java.math.BigInteger; +import static com.google.common.base.Preconditions.checkArgument; + +import io.singularitynet.sdk.ethereum.Address; + +@EqualsAndHashCode +@ToString +public class PaymentChannel { + + private final BigInteger channelId; + private final Address mpeContractAddress; + private final BigInteger nonce; + private final Address sender; + private final Address signer; + private final Address recipient; + private final byte[] paymentGroupId; + private final BigInteger value; + private final BigInteger expiration; + private final BigInteger spentAmount; + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder(this); + } + + private PaymentChannel(Builder builder) { + this.channelId = builder.channelId; + this.mpeContractAddress = builder.mpeContractAddress; + this.nonce = builder.nonce; + this.sender = builder.sender; + this.signer = builder.signer; + this.recipient = builder.recipient; + this.paymentGroupId = builder.paymentGroupId; + this.value = builder.value; + this.expiration = builder.expiration; + this.spentAmount = builder.spentAmount; + } + + public BigInteger getChannelId() { + return channelId; + } + + public Address getMpeContractAddress() { + return mpeContractAddress; + } + + public BigInteger getNonce() { + return nonce; + } + + public Address getSender() { + return sender; + } + + public Address getSigner() { + return signer; + } + + public Address getRecipient() { + return recipient; + } + + public byte[] getPaymentGroupId() { + return paymentGroupId; + } + + public BigInteger getValue() { + return value; + } + + public BigInteger getExpiration() { + return expiration; + } + + public BigInteger getSpentAmount() { + return spentAmount; + } + + public static class Builder { + + private BigInteger channelId; + private Address mpeContractAddress; + private BigInteger nonce; + private Address sender; + private Address signer; + private Address recipient; + private byte[] paymentGroupId; + private BigInteger value; + private BigInteger expiration; + private BigInteger spentAmount; + + private Builder() { + } + + private Builder(PaymentChannel object) { + this.channelId = object.channelId; + this.mpeContractAddress = object.mpeContractAddress; + this.nonce = object.nonce; + this.sender = object.sender; + this.signer = object.signer; + this.recipient = object.recipient; + this.paymentGroupId = object.paymentGroupId; + this.value = object.value; + this.expiration = object.expiration; + this.spentAmount = object.spentAmount; + } + + public Builder setChannelId(BigInteger channelId) { + this.channelId = channelId; + return this; + } + + public Builder setMpeContractAddress(Address mpeContractAddress) { + this.mpeContractAddress = mpeContractAddress; + return this; + } + + public Builder setNonce(BigInteger nonce) { + this.nonce = nonce; + return this; + } + + public Builder setSender(Address sender) { + this.sender = sender; + return this; + } + + public Builder setSigner(Address signer) { + this.signer = signer; + return this; + } + + public Builder setRecipient(Address recipient) { + this.recipient = recipient; + return this; + } + + public Builder setPaymentGroupId(byte[] paymentGroupId) { + checkArgument(paymentGroupId.length == 32, "Payment group id should be 32 bytes length"); + this.paymentGroupId = paymentGroupId; + return this; + } + + public Builder setValue(BigInteger value) { + this.value = value; + return this; + } + + public Builder setExpiration(BigInteger expiration) { + this.expiration = expiration; + return this; + } + + public Builder setSpentAmount(BigInteger spentAmount) { + this.spentAmount = spentAmount; + return this; + } + + public PaymentChannel build() { + return new PaymentChannel(this); + } + } +} diff --git a/src/main/java/io/singularitynet/sdk/mpe/PaymentChannelProvider.java b/src/main/java/io/singularitynet/sdk/mpe/PaymentChannelProvider.java new file mode 100644 index 00000000..a5ba1d78 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/mpe/PaymentChannelProvider.java @@ -0,0 +1,9 @@ +package io.singularitynet.sdk.mpe; + +import java.math.BigInteger; + +public interface PaymentChannelProvider { + + PaymentChannel getChannelById(BigInteger channelId); + +} diff --git a/src/main/java/io/singularitynet/sdk/registry/EndpointGroup.java b/src/main/java/io/singularitynet/sdk/registry/EndpointGroup.java index 72bd7d7e..fe2a6c9a 100644 --- a/src/main/java/io/singularitynet/sdk/registry/EndpointGroup.java +++ b/src/main/java/io/singularitynet/sdk/registry/EndpointGroup.java @@ -6,6 +6,9 @@ import java.util.List; import java.util.ArrayList; import com.google.gson.annotations.SerializedName; +import static com.google.common.base.Preconditions.checkArgument; + +import io.singularitynet.sdk.common.Utils; @EqualsAndHashCode @ToString @@ -14,6 +17,8 @@ public class EndpointGroup { private final String groupName; private final List pricing; private final List endpoints; + // TODO: replace by GroupId class to implement toString() and seemless JSON + // conversion @SerializedName("group_id") private final String paymentGroupId; public static Builder newBuilder() { @@ -43,8 +48,8 @@ public List getEndpoints() { return endpoints; } - public String getPaymentGroupId() { - return paymentGroupId; + public byte[] getPaymentGroupId() { + return Utils.base64ToBytes(paymentGroupId); } public static class Builder { @@ -74,13 +79,19 @@ public Builder addPricing(Pricing pricing) { return this; } - public Builder setEndpoints(URL endpoint) { + public Builder clearPricing() { + this.pricing.clear(); + return this; + } + + public Builder addEndpoint(URL endpoint) { this.endpoints.add(endpoint); return this; } - public Builder setPaymentGroupId(String paymentGroupId) { - this.paymentGroupId = paymentGroupId; + public Builder setPaymentGroupId(byte[] paymentGroupId) { + checkArgument(paymentGroupId.length == 32, "Payment group id should be 32 bytes length"); + this.paymentGroupId = Utils.bytesToBase64(paymentGroupId); return this; } diff --git a/src/main/java/io/singularitynet/sdk/registry/IpfsMetadataStorage.java b/src/main/java/io/singularitynet/sdk/registry/IpfsMetadataStorage.java index 672fa120..e1f8667f 100644 --- a/src/main/java/io/singularitynet/sdk/registry/IpfsMetadataStorage.java +++ b/src/main/java/io/singularitynet/sdk/registry/IpfsMetadataStorage.java @@ -4,7 +4,7 @@ import io.ipfs.api.IPFS; import io.ipfs.multihash.Multihash; -import static io.singularitynet.sdk.registry.Utils.*; +import static io.singularitynet.sdk.common.Utils.*; public class IpfsMetadataStorage implements MetadataStorage { diff --git a/src/main/java/io/singularitynet/sdk/registry/OrganizationMetadata.java b/src/main/java/io/singularitynet/sdk/registry/OrganizationMetadata.java new file mode 100644 index 00000000..15ae9edd --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/registry/OrganizationMetadata.java @@ -0,0 +1,77 @@ +package io.singularitynet.sdk.registry; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import java.util.List; +import java.util.ArrayList; +import com.google.gson.annotations.SerializedName; + +@EqualsAndHashCode +@ToString +public class OrganizationMetadata { + + private final String orgName; + private final String orgId; + @SerializedName("groups") private final List paymentGroups; + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder(this); + } + + private OrganizationMetadata(Builder builder) { + this.orgName = builder.orgName; + this.orgId = builder.orgId; + this.paymentGroups = builder.paymentGroups; + } + + public String getOrgName() { + return orgName; + } + + public String getOrgId() { + return orgId; + } + + public List getPaymentGroups() { + return paymentGroups; + } + + public static class Builder { + + private String orgName; + private String orgId; + private List paymentGroups = new ArrayList<>(); + + private Builder() { + } + + private Builder(OrganizationMetadata object) { + this.orgName = object.orgName; + this.orgId = object.orgId; + this.paymentGroups = object.paymentGroups; + } + + public Builder setOrgName(String orgName) { + this.orgName = orgName; + return this; + } + + public Builder setOrgId(String orgId) { + this.orgId = orgId; + return this; + } + + public Builder addPaymentGroup(PaymentGroup paymentGroup) { + this.paymentGroups.add(paymentGroup); + return this; + } + + public OrganizationMetadata build() { + return new OrganizationMetadata(this); + } + } +} diff --git a/src/main/java/io/singularitynet/sdk/registry/OrganizationRegistration.java b/src/main/java/io/singularitynet/sdk/registry/OrganizationRegistration.java new file mode 100644 index 00000000..698713b9 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/registry/OrganizationRegistration.java @@ -0,0 +1,77 @@ +package io.singularitynet.sdk.registry; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import java.net.URI; +import java.util.List; +import java.util.ArrayList; + +@EqualsAndHashCode +@ToString +public class OrganizationRegistration { + + private final String orgId; + private final URI metadataUri; + private final List serviceIds; + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder(this); + } + + private OrganizationRegistration(Builder builder) { + this.orgId = builder.orgId; + this.metadataUri = builder.metadataUri; + this.serviceIds = builder.serviceIds; + } + + public String getOrgId() { + return orgId; + } + + public URI getMetadataUri() { + return metadataUri; + } + + public List getServiceIds() { + return serviceIds; + } + + public static class Builder { + + private String orgId; + private URI metadataUri; + private List serviceIds = new ArrayList<>(); + + private Builder() { + } + + private Builder(OrganizationRegistration object) { + this.orgId = object.orgId; + this.metadataUri = object.metadataUri; + this.serviceIds = object.serviceIds; + } + + public Builder setOrgId(String orgId) { + this.orgId = orgId; + return this; + } + + public Builder setMetadataUri(URI metadataUri) { + this.metadataUri = metadataUri; + return this; + } + + public Builder addServiceId(String serviceId) { + this.serviceIds.add(serviceId); + return this; + } + + public OrganizationRegistration build() { + return new OrganizationRegistration(this); + } + } +} diff --git a/src/main/java/io/singularitynet/sdk/registry/PaymentDetails.java b/src/main/java/io/singularitynet/sdk/registry/PaymentDetails.java new file mode 100644 index 00000000..8398b9fe --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/registry/PaymentDetails.java @@ -0,0 +1,64 @@ +package io.singularitynet.sdk.registry; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import java.math.BigInteger; + +import io.singularitynet.sdk.ethereum.Address; + +@EqualsAndHashCode +@ToString +public class PaymentDetails { + + private final String paymentAddress; + private final BigInteger paymentExpirationThreshold; + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder(this); + } + + private PaymentDetails(Builder builder) { + this.paymentAddress = builder.paymentAddress; + this.paymentExpirationThreshold = builder.paymentExpirationThreshold; + } + + public Address getPaymentAddress() { + return new Address(paymentAddress); + } + + public BigInteger getPaymentExpirationThreshold() { + return paymentExpirationThreshold; + } + + public static class Builder { + + private String paymentAddress; + private BigInteger paymentExpirationThreshold; + + private Builder() { + } + + private Builder(PaymentDetails object) { + this.paymentAddress = object.paymentAddress; + this.paymentExpirationThreshold = object.paymentExpirationThreshold; + } + + public Builder setPaymentAddress(Address paymentAddress) { + this.paymentAddress = paymentAddress.toString(); + return this; + } + + public Builder setPaymentExpirationThreshold(BigInteger paymentExpirationThreshold) { + this.paymentExpirationThreshold = paymentExpirationThreshold; + return this; + } + + public PaymentDetails build() { + return new PaymentDetails(this); + } + } +} diff --git a/src/main/java/io/singularitynet/sdk/registry/PaymentGroup.java b/src/main/java/io/singularitynet/sdk/registry/PaymentGroup.java new file mode 100644 index 00000000..e2a8d589 --- /dev/null +++ b/src/main/java/io/singularitynet/sdk/registry/PaymentGroup.java @@ -0,0 +1,79 @@ +package io.singularitynet.sdk.registry; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import com.google.gson.annotations.SerializedName; +import static com.google.common.base.Preconditions.checkArgument; + +import io.singularitynet.sdk.common.Utils; + +@EqualsAndHashCode +@ToString +public class PaymentGroup { + + private final String groupName; + @SerializedName("group_id") private final String paymentGroupId; + @SerializedName("payment") private final PaymentDetails paymentDetails; + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder(this); + } + + private PaymentGroup(Builder builder) { + this.groupName = builder.groupName; + this.paymentGroupId = builder.paymentGroupId; + this.paymentDetails = builder.paymentDetails; + } + + public String getGroupName() { + return groupName; + } + + public byte[] getPaymentGroupId() { + return Utils.base64ToBytes(paymentGroupId); + } + + public PaymentDetails getPaymentDetails() { + return paymentDetails; + } + + public static class Builder { + + private String groupName; + private String paymentGroupId; + private PaymentDetails paymentDetails; + + private Builder() { + } + + private Builder(PaymentGroup object) { + this.groupName = object.groupName; + this.paymentGroupId = object.paymentGroupId; + this.paymentDetails = object.paymentDetails; + } + + public Builder setGroupName(String groupName) { + this.groupName = groupName; + return this; + } + + public Builder setPaymentGroupId(byte[] paymentGroupId) { + checkArgument(paymentGroupId.length == 32, "Payment group id should be 32 bytes length"); + this.paymentGroupId = Utils.bytesToBase64(paymentGroupId); + return this; + } + + public Builder setPaymentDetails(PaymentDetails paymentDetails) { + this.paymentDetails = paymentDetails; + return this; + } + + public PaymentGroup build() { + return new PaymentGroup(this); + } + } +} diff --git a/src/main/java/io/singularitynet/sdk/registry/Pricing.java b/src/main/java/io/singularitynet/sdk/registry/Pricing.java index 3b39b4cf..8757ac71 100644 --- a/src/main/java/io/singularitynet/sdk/registry/Pricing.java +++ b/src/main/java/io/singularitynet/sdk/registry/Pricing.java @@ -8,7 +8,7 @@ @ToString public class Pricing { - private final PriceModel priceModel; + private final String priceModel; private final BigInteger priceInCogs; public static Builder newBuilder() { @@ -25,7 +25,9 @@ private Pricing(Builder builder) { } public PriceModel getPriceModel() { - return priceModel; + // TODO: remove additional conversion: either change + // priceModel type to enum or remove enum at all + return Enum.valueOf(PriceModel.class, priceModel.toUpperCase()); } public BigInteger getPriceInCogs() { @@ -34,7 +36,7 @@ public BigInteger getPriceInCogs() { public static class Builder { - private PriceModel priceModel; + private String priceModel; private BigInteger priceInCogs; private Builder() { @@ -46,10 +48,11 @@ private Builder(Pricing object) { } public Builder setPriceModel(PriceModel priceModel) { - this.priceModel = priceModel; + this.priceModel = priceModel.toString(); return this; } + // TODO: replace BigInteger by Price type public Builder setPriceInCogs(BigInteger priceInCogs) { this.priceInCogs = priceInCogs; return this; diff --git a/src/main/java/io/singularitynet/sdk/registry/RegistryContract.java b/src/main/java/io/singularitynet/sdk/registry/RegistryContract.java index f2344a86..0dde40b2 100644 --- a/src/main/java/io/singularitynet/sdk/registry/RegistryContract.java +++ b/src/main/java/io/singularitynet/sdk/registry/RegistryContract.java @@ -6,7 +6,7 @@ import java.util.List; import io.singularitynet.sdk.contracts.Registry; -import static io.singularitynet.sdk.registry.Utils.*; +import static io.singularitynet.sdk.common.Utils.*; public class RegistryContract { @@ -16,10 +16,26 @@ public RegistryContract(Registry registry) { this.registry = registry; } + public Optional getOrganizationById(String orgId) { + return wrapExceptions(() -> { + Tuple7, List, List> result = + registry.getOrganizationById(strToBytes32(orgId)).send(); + OrganizationRegistration.Builder builder = OrganizationRegistration.newBuilder() + .setOrgId(bytes32ToStr(result.component2())) + .setMetadataUri(new URI(bytesToStr(result.component3()))); + for (byte[] serviceId : result.component6()) { + builder.addServiceId(bytes32ToStr(serviceId)); + } + // TODO: empty result case + return Optional.of(builder.build()); + }); + } + public Optional getServiceRegistrationById(String orgId, String serviceId) { return wrapExceptions(() -> { Tuple4> result = registry.getServiceRegistrationById(strToBytes32(orgId), strToBytes32(serviceId)).send(); + // TODO: empty result case return Optional.of(ServiceRegistration.newBuilder() .setServiceId(bytes32ToStr(result.component2())) .setMetadataUri(new URI(bytesToStr(result.component3()))) diff --git a/src/main/java/io/singularitynet/sdk/registry/RegistryMetadataProvider.java b/src/main/java/io/singularitynet/sdk/registry/RegistryMetadataProvider.java index 68b1bb92..4e66ce80 100644 --- a/src/main/java/io/singularitynet/sdk/registry/RegistryMetadataProvider.java +++ b/src/main/java/io/singularitynet/sdk/registry/RegistryMetadataProvider.java @@ -1,7 +1,7 @@ package io.singularitynet.sdk.registry; import com.google.gson.*; -import static io.singularitynet.sdk.registry.Utils.*; +import static io.singularitynet.sdk.common.Utils.*; public class RegistryMetadataProvider implements MetadataProvider { diff --git a/src/main/java/io/singularitynet/sdk/registry/ServiceMetadata.java b/src/main/java/io/singularitynet/sdk/registry/ServiceMetadata.java index ff399563..3ecec724 100644 --- a/src/main/java/io/singularitynet/sdk/registry/ServiceMetadata.java +++ b/src/main/java/io/singularitynet/sdk/registry/ServiceMetadata.java @@ -6,6 +6,8 @@ import java.util.ArrayList; import com.google.gson.annotations.SerializedName; +import io.singularitynet.sdk.ethereum.Address; + @EqualsAndHashCode @ToString public class ServiceMetadata { @@ -32,8 +34,8 @@ public String getDisplayName() { return displayName; } - public String getMpeAddress() { - return mpeAddress; + public Address getMpeAddress() { + return new Address(mpeAddress); } public List getEndpointGroups() { @@ -60,16 +62,21 @@ public Builder setDisplayName(String displayName) { return this; } - public Builder setMpeAddress(String mpeAddress) { - this.mpeAddress = mpeAddress; + public Builder setMpeAddress(Address mpeAddress) { + this.mpeAddress = mpeAddress.toString(); return this; } - public Builder addEndpointGroups(EndpointGroup endpointGroup) { + public Builder addEndpointGroup(EndpointGroup endpointGroup) { this.endpointGroups.add(endpointGroup); return this; } + public Builder clearEndpointGroups() { + this.endpointGroups.clear(); + return this; + } + public ServiceMetadata build() { return new ServiceMetadata(this); } diff --git a/src/main/java/io/singularitynet/sdk/registry/Utils.java b/src/main/java/io/singularitynet/sdk/registry/Utils.java deleted file mode 100644 index 49fa8fa9..00000000 --- a/src/main/java/io/singularitynet/sdk/registry/Utils.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.singularitynet.sdk.registry; - -import static java.nio.charset.StandardCharsets.UTF_8; -import java.util.concurrent.Callable; - -public class Utils { - - public static byte[] strToBytes(String str) { - return str.getBytes(UTF_8); - } - - public static byte[] strToBytes32(String str) { - byte[] bytes32 = new byte[32]; - int i = 0; - for (byte b : str.getBytes(UTF_8)) { - bytes32[i++] = b; - } - return bytes32; - } - - public static String bytes32ToStr(byte[] bytes) { - String full = new String(bytes, UTF_8); - return full.substring(0, full.indexOf(0)); - } - - public static String bytesToStr(byte[] bytes) { - return new String(bytes, UTF_8); - } - - public static T wrapExceptions(Callable callable) { - try { - return callable.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - -} diff --git a/src/test/java/io/singularitynet/sdk/common/UtilsTest.java b/src/test/java/io/singularitynet/sdk/common/UtilsTest.java new file mode 100644 index 00000000..0035347a --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/common/UtilsTest.java @@ -0,0 +1,112 @@ +package io.singularitynet.sdk.common; + +import org.junit.*; +import static org.junit.Assert.*; +import org.junit.rules.ExpectedException; + +import java.math.BigInteger; +import java.util.Arrays; + +public class UtilsTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void wrapExceptionRethrowsCheckedException() { + thrown.expect(RuntimeException.class); + thrown.expectMessage("test exception"); + + Utils.wrapExceptions(() -> { + throw new Exception("test exception"); + }); + } + + @Test + public void strToBytes32ChecksArgumentLength() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Passed string length exceeds 32 bytes"); + + Utils.strToBytes32("012345678901234567890123456789012"); + } + + @Test + public void strToBytes32AddsZeroPadding() { + byte[] bytes32 = Utils.strToBytes32(""); + + assertArrayEquals("Bytes32 from empty string", new byte[32], bytes32); + } + + @Test + public void bytes32ToStrCheckArgumentNotTooLong() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Passed array length is not equal to 32 bytes"); + + Utils.bytes32ToStr(new byte[33]); + } + + @Test + public void bytes32ToStrCheckArgumentNotTooShort() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Passed array length is not equal to 32 bytes"); + + Utils.bytes32ToStr(new byte[31]); + } + + @Test + public void bytes32ToStr32CharLength() { + byte[] bytes = new byte[32]; + Arrays.fill(bytes, (byte)'x'); + + String str = Utils.bytes32ToStr(bytes); + + assertEquals("Max length string", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", str); + } + + @Test + public void bigIntToBytes32() { + byte[] bytes32 = Utils.bigIntToBytes32(BigInteger.valueOf(0x1234)); + + assertArrayEquals("BigInteger converted to 32 bytes", + new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34 }, + bytes32); + } + + @Test + public void bytes32ToBigInt() { + BigInteger bigint = Utils.bytes32ToBigInt(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34 }); + + assertEquals("BigInteger converted from 32 bytes", + BigInteger.valueOf(0x1234), + bigint); + } + + @Test + public void bytesToStrTrimTrailingZeros() { + byte[] bytes = { 't', 'e', 's', 't', 0, 0, 0 }; + + String str = Utils.bytesToStr(bytes); + + assertEquals("String from bytes with trailing zeros", "test", str); + } + + @Test + public void bytesToStr() { + byte[] bytes = "test".getBytes(); + + String str = Utils.bytesToStr(bytes); + + assertEquals("String from bytes", "test", str); + } + + @Test + public void strToBytes() { + String str = "test"; + + byte[] bytes = Utils.strToBytes(str); + + assertArrayEquals("Bytes from string", new byte[] { 't', 'e', 's', 't' }, + bytes); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/daemon/DaemonMock.java b/src/test/java/io/singularitynet/sdk/daemon/DaemonMock.java new file mode 100644 index 00000000..6711f849 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/daemon/DaemonMock.java @@ -0,0 +1,66 @@ +package io.singularitynet.sdk.daemon; + +import java.util.Map; +import java.util.HashMap; +import java.math.BigInteger; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.util.Optional; +import io.grpc.*; +import io.grpc.stub.StreamObserver; +import com.google.protobuf.ByteString; + +import io.singularitynet.daemon.escrow.*; +import io.singularitynet.sdk.common.Utils; +import io.singularitynet.sdk.mpe.PaymentChannel; + +public class DaemonMock extends PaymentChannelStateServiceGrpc.PaymentChannelStateServiceImplBase + implements ServerInterceptor { + + private final List payments = Collections.synchronizedList(new ArrayList<>()); + private final Map channelStates = new HashMap<>(); + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, + ServerCallHandler next) { + Optional payment = PaymentSerializer.fromMetadata(headers); + if (payment.isPresent()) { + payments.add(payment.get()); + } + return next.startCall(call, headers); + } + + public List getPayments() { + return payments; + } + + @Override + public void getChannelState(StateService.ChannelStateRequest request, + StreamObserver callback) { + BigInteger channelId = Utils.bytes32ToBigInt(request.getChannelId().toByteArray()); + StateService.ChannelStateReply reply = channelStates.get(channelId); + if (reply == null) { + callback.onError(new Throwable("No such channel")); + } + + callback.onNext(reply); + callback.onCompleted(); + } + + public void setChannelState(BigInteger channelId, PaymentChannelStateReply reply) { + channelStates.put(channelId, StateService.ChannelStateReply.newBuilder() + .setCurrentNonce(ByteString.copyFrom(Utils.bigIntToBytes32(reply.getCurrentNonce()))) + .setCurrentSignedAmount(ByteString.copyFrom(Utils.bigIntToBytes32(reply.getCurrentSignedAmount()))) + .setCurrentSignature(ByteString.copyFrom(reply.getCurrentSignature())) + .build()); + } + + public void setChannelStateIsAbsent(PaymentChannel channel) { + channelStates.put(channel.getChannelId(), StateService.ChannelStateReply.newBuilder() + .setCurrentNonce(ByteString.copyFrom(Utils.bigIntToBytes32(channel.getNonce()))) + .build()); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/daemon/PaymentChannelStateServiceTest.java b/src/test/java/io/singularitynet/sdk/daemon/PaymentChannelStateServiceTest.java new file mode 100644 index 00000000..3e2c2c48 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/daemon/PaymentChannelStateServiceTest.java @@ -0,0 +1,46 @@ +package io.singularitynet.sdk.daemon; + +import org.junit.*; +import static org.junit.Assert.*; + +import java.math.BigInteger; +import com.google.protobuf.ByteString; + +import io.singularitynet.sdk.test.Environment; +import io.singularitynet.sdk.ethereum.Signer; +import io.singularitynet.sdk.ethereum.PrivateKeyIdentity; +import io.singularitynet.sdk.mpe.PaymentChannel; +import io.singularitynet.daemon.escrow.StateService.ChannelStateRequest; +import io.singularitynet.sdk.common.Utils; +import io.singularitynet.sdk.ethereum.Address; + +public class PaymentChannelStateServiceTest { + + private Environment env; + + @Before + public void setUp() { + env = Environment.env(); + } + + @Test + public void signChannelStateRequest() { + String privateKey = "89765001819765816734960087977248703971879862101523844953632906408104497565820"; + long ethereumBlockNumber = 53; + Address mpeAddress = new Address("0xf25186B5081Ff5cE73482AD761DB0eB0d25abfBF"); + long channelId = 42; + + env.setCurrentEthereumBlockNumber(ethereumBlockNumber); + Signer signer = new PrivateKeyIdentity(new BigInteger(privateKey)); + PaymentChannelStateService.MessageSigningHelper helper = + new PaymentChannelStateService.MessageSigningHelper(mpeAddress, env.ethereum(), signer); + ChannelStateRequest.Builder request = ChannelStateRequest.newBuilder() + .setChannelId(ByteString.copyFrom(Utils.bigIntToBytes32(BigInteger.valueOf(channelId)))); + + helper.signChannelStateRequest(request); + + assertEquals("Signature", "kegbvf4a+kzqDiIkDDsWIZu2EFqbR5dQzKrSmy3w6uxhg+NuOFc09wwXSwUiO46R5FN+XQ/Yjtwgxyck4K9OhRs=", + Utils.bytesToBase64(request.getSignature().toByteArray())); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/ethereum/AddressTest.java b/src/test/java/io/singularitynet/sdk/ethereum/AddressTest.java new file mode 100644 index 00000000..cd479294 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/ethereum/AddressTest.java @@ -0,0 +1,26 @@ +package io.singularitynet.sdk.ethereum; + +import org.junit.*; +import static org.junit.Assert.*; + +public class AddressTest { + + @Test + public void addressToByteArrayPrefix() { + byte[] result = new Address("0xf25186B5081Ff5cE73482AD761DB0eB0d25abfBF").toByteArray(); + + assertArrayEquals("Contract address converted", + new byte[] { (byte) 0xf2, 0x51, (byte) 0x86, (byte) 0xB5, 0x08, 0x1F, (byte) 0xf5, (byte) 0xcE, 0x73, 0x48, 0x2A, (byte) 0xD7, 0x61, (byte) 0xDB, 0x0e, (byte) 0xB0, (byte) 0xd2, (byte) 0x5a, (byte) 0xbf, (byte) 0xBF }, + result); + } + + @Test + public void addressToByteArrayNoPrefix() { + byte[] result = new Address("f25186B5081Ff5cE73482AD761DB0eB0d25abfBF").toByteArray(); + + assertArrayEquals("Contract address converted", + new byte[] { (byte) 0xf2, 0x51, (byte) 0x86, (byte) 0xB5, 0x08, 0x1F, (byte) 0xf5, (byte) 0xcE, 0x73, 0x48, 0x2A, (byte) 0xD7, 0x61, (byte) 0xDB, 0x0e, (byte) 0xB0, (byte) 0xd2, (byte) 0x5a, (byte) 0xbf, (byte) 0xBF }, + result); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/ethereum/MnemonicIndentityTest.java b/src/test/java/io/singularitynet/sdk/ethereum/MnemonicIndentityTest.java new file mode 100644 index 00000000..292b0df3 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/ethereum/MnemonicIndentityTest.java @@ -0,0 +1,36 @@ +package io.singularitynet.sdk.ethereum; + +import static org.junit.Assert.*; +import org.junit.*; + +import io.singularitynet.sdk.common.Utils; + +public class MnemonicIndentityTest { + + private static String mnemonic = "insect lottery stable theme shrimp expose match frog entry always viable cabbage mechanic cinnamon spread"; + private static byte[] message = Utils.strToBytes32("Some message"); + + @Test + public void generateMnemonicIdentityWallet0() { + PrivateKeyIdentity expected = fromBase64Key("pAIRj43MIe6/Yj6AigjI0X8jYzDSk929nUSkRBRJcVA="); + + MnemonicIdentity actual = new MnemonicIdentity(mnemonic, 0); + + assertArrayEquals("Signature", expected.sign(message), actual.sign(message)); + } + + @Test + public void generateMnemonicIdentityWallet42() { + PrivateKeyIdentity expected = fromBase64Key("gczjSyFyWK024XVHDipAsFr3EP3v3NtDanNsrO4O1D8="); + + MnemonicIdentity actual = new MnemonicIdentity(mnemonic, 42); + + assertArrayEquals("Signature", expected.sign(message), actual.sign(message)); + } + + private static PrivateKeyIdentity fromBase64Key(String key) { + byte[] privateKey = Utils.base64ToBytes(key); + return new PrivateKeyIdentity(privateKey); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/ethereum/PrivateKeyIdentityTest.java b/src/test/java/io/singularitynet/sdk/ethereum/PrivateKeyIdentityTest.java new file mode 100644 index 00000000..8b1f881e --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/ethereum/PrivateKeyIdentityTest.java @@ -0,0 +1,19 @@ +package io.singularitynet.sdk.ethereum; + +import org.junit.*; +import static org.junit.Assert.*; + +import io.singularitynet.sdk.common.Utils; + +public class PrivateKeyIdentityTest { + + @Test + public void signPayment() { + PrivateKeyIdentity identity = new PrivateKeyIdentity(Utils.base64ToBytes("1PeCDRD7vLjqiGoHl7A+yPuJIy8TdbNc1vxOyuPjxBM=")); + + byte[] signature = identity.sign(Utils.base64ToBytes("X19NUEVfY2xhaW1fbWVzc2FnZfJRhrUIH/XOc0gq12HbDrDSWr+/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADA5")); + + assertEquals("Message signature", "1uz5pkIREtQ9egzhv8khEmqQPTtu3EjYBHpllmBlnVAOU+MxK3vi6U7ECaFNkEJA0Bv3bqIGraaKOLwbJHwsFBw=", Utils.bytesToBase64(signature)); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/mpe/EscrowPaymentTest.java b/src/test/java/io/singularitynet/sdk/mpe/EscrowPaymentTest.java new file mode 100644 index 00000000..b85236cb --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/mpe/EscrowPaymentTest.java @@ -0,0 +1,33 @@ +package io.singularitynet.sdk.mpe; + +import static org.junit.Assert.*; +import org.junit.*; + +import java.math.BigInteger; + +import io.singularitynet.sdk.common.Utils; +import io.singularitynet.sdk.ethereum.Address; +import io.singularitynet.sdk.ethereum.PrivateKeyIdentity; + +public class EscrowPaymentTest { + + @Test + public void messageSignedCorrectly() { + PaymentChannel channel = PaymentChannel.newBuilder() + .setMpeContractAddress(new Address("0xf25186B5081Ff5cE73482AD761DB0eB0d25abfBF")) + .setChannelId(BigInteger.valueOf(42)) + .setNonce(BigInteger.valueOf(3)) + .build(); + PrivateKeyIdentity signer = new PrivateKeyIdentity(Utils.base64ToBytes("Bvk3Bf8PnVj6kwE1IrG/gHXUpYO+chDKf4mu1FTilkI=")); + + EscrowPayment payment = EscrowPayment.newBuilder() + .setPaymentChannel(channel) + .setAmount(BigInteger.valueOf(12345)) + .setSigner(signer) + .build(); + + assertEquals("Escrow payment", "vupXehLD4yd+GhENa+slIPVsd2U8/V771TtlhiJE7YJ7jBM07ECTL8OEOb9C4BfFUEYmw2w2/7YcNAPOHOuKdBs=", + Utils.bytesToBase64(payment.getSignature())); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/mpe/MultiPartyEscrowMock.java b/src/test/java/io/singularitynet/sdk/mpe/MultiPartyEscrowMock.java new file mode 100644 index 00000000..3d920112 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/mpe/MultiPartyEscrowMock.java @@ -0,0 +1,37 @@ +package io.singularitynet.sdk.mpe; + +import java.math.BigInteger; +import org.web3j.protocol.core.*; +import org.web3j.tuples.generated.*; +import io.singularitynet.sdk.contracts.MultiPartyEscrow; +import static org.mockito.Mockito.*; + +import static io.singularitynet.sdk.common.Utils.*; +import io.singularitynet.sdk.ethereum.Address; + +public class MultiPartyEscrowMock { + + private final MultiPartyEscrow mpe = mock(MultiPartyEscrow.class); + + public MultiPartyEscrow get() { + return mpe; + } + + public void addPaymentChannel(PaymentChannel paymentChannel) { + when(mpe.channels(eq(paymentChannel.getChannelId()))). + thenReturn(new RemoteFunctionCall<>(null, () -> { + return new Tuple7<>(paymentChannel.getNonce(), + paymentChannel.getSender().toString(), + paymentChannel.getSigner().toString(), + paymentChannel.getRecipient().toString(), + paymentChannel.getPaymentGroupId(), + paymentChannel.getValue(), + paymentChannel.getExpiration()); + })); + } + + public void setContractAddress(Address address) { + when(mpe.getContractAddress()).thenReturn(address.toString()); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/registry/IpfsMock.java b/src/test/java/io/singularitynet/sdk/registry/IpfsMock.java index 0e683ff6..c462fe84 100644 --- a/src/test/java/io/singularitynet/sdk/registry/IpfsMock.java +++ b/src/test/java/io/singularitynet/sdk/registry/IpfsMock.java @@ -4,8 +4,10 @@ import io.ipfs.multihash.Multihash; import javax.json.*; import static org.mockito.Mockito.*; +import java.net.URI; +import java.net.URL; -import static io.singularitynet.sdk.registry.Utils.*; +import static io.singularitynet.sdk.common.Utils.*; public class IpfsMock { @@ -15,51 +17,91 @@ public IPFS get() { return ipfs; } - public ReturnMock cat(String hash) { - return new ReturnMock() { - public IpfsMock returns(JsonObjectBuilder value) { - return wrapExceptions(() -> { - when(ipfs.cat(eq(Multihash.fromBase58(hash)))) - .thenReturn(strToBytes(value.build().toString())); - return IpfsMock.this; - }); - } - }; - } - - public static interface ReturnMock { - IpfsMock returns(T value); - } - - public static JsonObjectBuilder serviceMetadataJson(int port) { - return Json.createObjectBuilder() + public URI addService(ServiceMetadata metadata) { + JsonObjectBuilder rootBuilder = Json.createObjectBuilder() .add("version", "1") - .add("display_name", "Test Service Name") + .add("display_name", metadata.getDisplayName()) .add("encoding", "proto") .add("service_type", "grpc") .add("model_ipfs_hash", "QmR3anSdm4s13iLt3zzyrSbtvCDJNwhkrYG6yFGFHXBznb") - .add("mpe_address", "0x8FB1dC8df86b388C7e00689d1eCb533A160B4D0C") - .add("groups", Json.createArrayBuilder() - .add(Json.createObjectBuilder() - .add("group_name", "default_group") - .add("pricing", Json.createArrayBuilder() - .add(Json.createObjectBuilder() - .add("price_model", "fixed_price") - .add("price_in_cogs", 1) - .add("default", true) - .build()) - .build()) - .add("endpoints", Json.createArrayBuilder() - .add("http://localhost:" + port) - .build()) - .add("group_id", "m5FKWq4hW0foGW5qSbzGSjgZRuKs7A1ZwbIrJ9e96rc=") - .build()) - .build()) - .add("assets", Json.createObjectBuilder().build()) + .add("mpe_address", metadata.getMpeAddress().toString()); + JsonArrayBuilder groupsBuilder = Json.createArrayBuilder(); + for (EndpointGroup group : metadata.getEndpointGroups()) { + JsonObjectBuilder groupBuilder = Json.createObjectBuilder() + .add("group_name", group.getGroupName()); + + JsonArrayBuilder pricingBuilder = Json.createArrayBuilder(); + boolean dflt = true; + for (Pricing price : group.getPricing()) { + JsonObjectBuilder priceBuilder = Json.createObjectBuilder() + .add("price_model", price.getPriceModel().toString().toLowerCase()) + .add("price_in_cogs", price.getPriceInCogs().toString()); + if (dflt) { + priceBuilder.add("default", true); + dflt = false; + } + pricingBuilder.add(priceBuilder); + } + groupBuilder.add("pricing", pricingBuilder); + + JsonArrayBuilder endpointsBuilder = Json.createArrayBuilder(); + for (URL endpoint : group.getEndpoints()) { + endpointsBuilder.add(endpoint.toString()); + } + groupBuilder.add("endpoints", endpointsBuilder) + .add("group_id", bytesToBase64(group.getPaymentGroupId())); + groupsBuilder.add(groupBuilder); + } + rootBuilder.add("groups", groupsBuilder) + .add("assets", Json.createObjectBuilder()) .add("service_description", Json.createObjectBuilder() .add("url", "https://singnet.github.io/dnn-model-services/users_guide/i3d-video-action-recognition.html") - .add("description", "This service uses I3D to perform action recognition on videos.") - .build()); + .add("description", "This service uses I3D to perform action recognition on videos.")); + + return wrapExceptions(() -> { + String json = rootBuilder.build().toString(); + String hash = "QmR3anSdm4s13iLt3zzyrSbtvCDJNwhkrYG6yFGFHXBznb"; + when(ipfs.cat(eq(Multihash.fromBase58(hash)))) + .thenReturn(strToBytes(json)); + return new URI("ipfs://" + hash); + }); } + + public URI addOrganization(OrganizationMetadata metadata) { + JsonObjectBuilder rootBuilder = Json.createObjectBuilder() + .add("org_name", metadata.getOrgName()) + .add("org_id", metadata.getOrgId()); + + JsonArrayBuilder groupsBuilder = Json.createArrayBuilder(); + for (PaymentGroup group : metadata.getPaymentGroups()) { + PaymentDetails paymentDetails = group.getPaymentDetails(); + JsonObjectBuilder groupBuilder = Json.createObjectBuilder() + .add("group_name", group.getGroupName()) + .add("group_id", bytesToBase64(group.getPaymentGroupId())) + .add("payment", Json.createObjectBuilder() + .add("payment_address", paymentDetails.getPaymentAddress().toString()) + .add("payment_expiration_threshold", paymentDetails.getPaymentExpirationThreshold().toString()) + .add("payment_channel_storage_type", "etcd") + .add("payment_channel_storage_client", Json.createObjectBuilder() + .add("connection_timeout", "100s") + .add("request_timeout", "5s") + .add("endpoints", Json.createArrayBuilder() + .add("https://snet-etcd.singularitynet.io:2379") + ) + ) + ); + groupsBuilder.add(groupBuilder); + } + rootBuilder.add("groups", groupsBuilder); + + return wrapExceptions(() -> { + String json = rootBuilder.build().toString(); + String hash = "QmSesBRhz67FRixd3mGMNmQE5sNyZxdDgcNMEBmmhHk2X6"; + when(ipfs.cat(eq(Multihash.fromBase58(hash)))) + .thenReturn(strToBytes(json)); + return new URI("ipfs://" + hash); + }); + } + } diff --git a/src/test/java/io/singularitynet/sdk/registry/RegistryMock.java b/src/test/java/io/singularitynet/sdk/registry/RegistryMock.java index ea44a310..503bf811 100644 --- a/src/test/java/io/singularitynet/sdk/registry/RegistryMock.java +++ b/src/test/java/io/singularitynet/sdk/registry/RegistryMock.java @@ -1,13 +1,14 @@ package io.singularitynet.sdk.registry; -import java.util.List; -import java.util.Collections; import org.web3j.protocol.core.*; import org.web3j.tuples.generated.*; import io.singularitynet.sdk.contracts.Registry; import static org.mockito.Mockito.*; +import static java.util.stream.Collectors.toList; +import java.util.Collections; -import static io.singularitynet.sdk.registry.Utils.*; +import static io.singularitynet.sdk.common.Utils.*; +import io.singularitynet.sdk.common.Utils; public class RegistryMock { @@ -17,58 +18,37 @@ public Registry get() { return registry; } - public ReturnMock getServiceRegistrationById(String orgId, String serviceId) { - return new ReturnMock() { - public RegistryMock returns(GetServiceRegistrationByIdResultBuider value) { - when(registry.getServiceRegistrationById(eq(strToBytes32(orgId)), eq(strToBytes32(serviceId)))) - .thenReturn(value.build()); - return RegistryMock.this; - } - }; + public void addServiceRegistration(String orgId, String serviceId, + ServiceRegistration registration) { + when(registry.getServiceRegistrationById(eq(strToBytes32(orgId)), + eq(strToBytes32(serviceId)))) + .thenReturn(new RemoteFunctionCall<>(null, + () -> { + return new Tuple4<>(true, + strToBytes32(registration.getServiceId()), + strToBytes(registration.getMetadataUri().toString()), + registration.getTags().stream().map(Utils::strToBytes32).collect(toList())); + }) + ); } - public static interface ReturnMock { - RegistryMock returns(T value); + public void addOrganizationRegistration(String orgId, + OrganizationRegistration registration) { + when(registry.getOrganizationById(eq(strToBytes32(orgId)))) + .thenReturn(new RemoteFunctionCall<>(null, + () -> { + return new Tuple7<>(true, + strToBytes32(registration.getOrgId()), + strToBytes(registration.getMetadataUri().toString()), + "0xfA8a01E837c30a3DA3Ea862e6dB5C6232C9b800A", + Collections.EMPTY_LIST, + registration.getServiceIds().stream().map(Utils::strToBytes32).collect(toList()), + Collections.EMPTY_LIST + ); + }) + ); } - public static class GetServiceRegistrationByIdResultBuider { - - private Boolean found; - private byte[] id; - private byte[] metadataUri; - - public GetServiceRegistrationByIdResultBuider() { - } - - public GetServiceRegistrationByIdResultBuider setFound(Boolean found) { - this.found = found; - return this; - } - - public GetServiceRegistrationByIdResultBuider setId(String id) { - this.id = strToBytes32(id); - return this; - } - - public GetServiceRegistrationByIdResultBuider setMetadataUri(String metadataUri) { - this.metadataUri = strToBytes(metadataUri); - return this; - } - - public RemoteFunctionCall>> build() { - return new RemoteFunctionCall<>(null, - () -> { - return new Tuple4<>(found, id, metadataUri, Collections.EMPTY_LIST); - }); - } - } - - public static GetServiceRegistrationByIdResultBuider serviceRegistration() { - return new GetServiceRegistrationByIdResultBuider() - .setFound(true) - .setId("test-service-id") - .setMetadataUri("ipfs://QmR3anSdm4s13iLt3zzyrSbtvCDJNwhkrYG6yFGFHXBznb"); - } } diff --git a/src/test/java/io/singularitynet/sdk/test/Environment.java b/src/test/java/io/singularitynet/sdk/test/Environment.java new file mode 100644 index 00000000..564cabb3 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/test/Environment.java @@ -0,0 +1,257 @@ +package io.singularitynet.sdk.test; + +import static org.mockito.Mockito.*; + +import java.net.URI; +import java.util.*; +import java.math.BigInteger; +import org.web3j.protocol.core.Ethereum; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.EthBlockNumber; + +import io.singularitynet.sdk.common.Utils; +import io.singularitynet.sdk.registry.*; +import io.singularitynet.sdk.mpe.*; +import io.singularitynet.sdk.daemon.*; +import io.singularitynet.sdk.ethereum.*; + +public class Environment { + + private Ethereum ethereum = mock(Ethereum.class); + private RegistryMock registry = new RegistryMock(); + private IpfsMock ipfs = new IpfsMock(); + private MultiPartyEscrowMock mpe = new MultiPartyEscrowMock(); + private DaemonMock daemon = new DaemonMock(); + private TestServer server = TestServer.start(daemon); + + private Address mpeAddress = randomAddress(); + + private Environment() { + ethereum = newEthereumMock(); + registry = new RegistryMock(); + ipfs = new IpfsMock(); + mpe = new MultiPartyEscrowMock(); + daemon = new DaemonMock(); + server = TestServer.start(daemon); + + mpeAddress = randomAddress(); + mpe.setContractAddress(mpeAddress); + } + + public static Environment env() { + return new Environment(); + } + + public Ethereum ethereum() { + return ethereum; + } + + public RegistryMock registry() { + return registry; + } + + public IpfsMock ipfs() { + return ipfs; + } + + public MultiPartyEscrowMock mpe() { + return mpe; + } + + public DaemonMock daemon() { + return daemon; + } + + public TestServer server() { + return server; + } + + private static Ethereum newEthereumMock() { + Ethereum ethereum = mock(Ethereum.class); + setCurrentEthereumBlockNumber(ethereum, (long)(1000 * Math.random())); + return ethereum; + } + + public void updateMocks() { + registerServices(); + registerOrganizations(); + } + + public void setCurrentEthereumBlockNumber(long blockNumber) { + setCurrentEthereumBlockNumber(this.ethereum, blockNumber); + } + + private static void setCurrentEthereumBlockNumber(Ethereum ethereum, long blockNumber) { + Utils.wrapExceptions(() -> { + BigInteger curEthBlock = BigInteger.valueOf(blockNumber); + EthBlockNumber ethBlockNumber = mock(EthBlockNumber.class); + when(ethBlockNumber.getBlockNumber()).thenReturn(curEthBlock); + Request ethBlockNumberReq = mock(Request.class); + when(ethBlockNumberReq.send()).thenReturn(ethBlockNumber); + when(ethereum.ethBlockNumber()).thenReturn(ethBlockNumberReq); + return null; + }); + } + + private static final int ADDRESS_LENGTH = 20; + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + public static Address randomAddress() { + StringBuffer address = new StringBuffer(); + address.append("0x"); + for (int i = 0; i < ADDRESS_LENGTH * 2; ++i) { + int halfByte = (int) (Math.random() * 16); + address.append(HEX_ARRAY[halfByte]); + } + return new Address(address.toString()); + } + + public static byte[] randomUint256() { + byte[] uint256 = new byte[32]; + for (int i = 0; i < 32; ++i) { + uint256[i] = (byte) (Math.random() * 256); + } + return uint256; + } + + private Map signerByAddress = new HashMap<>(); + + public Signer newSigner() { + Signer signer = new PrivateKeyIdentity(Utils.base64ToBytes("1PeCDRD7vLjqiGoHl7A+yPuJIy8TdbNc1vxOyuPjxBM=")); + signerByAddress.put(signer.getAddress(), signer); + return signer; + } + + private Map paymentGroupById = new HashMap<>(); + + public PaymentGroup.Builder newPaymentGroup() { + byte[] groupId = randomUint256(); + PaymentGroup.Builder paymentGroup = PaymentGroup.newBuilder() + .setGroupName("default_group") + .setPaymentGroupId(groupId) + .setPaymentDetails(PaymentDetails.newBuilder() + .setPaymentAddress(new Address("0xfA8a01E837c30a3DA3Ea862e6dB5C6232C9b800A")) + .setPaymentExpirationThreshold(BigInteger.valueOf(100)) + .build()); + paymentGroupById.put(Utils.bytesToBase64(groupId), paymentGroup); + return paymentGroup; + } + + private Map organizationMetadataById = new HashMap<>(); + + public OrganizationMetadata.Builder newOrganizationMetadata(String orgId) { + OrganizationMetadata.Builder metadata = OrganizationMetadata.newBuilder() + .setOrgName("Test Organization") + .setOrgId(orgId) + .addPaymentGroup(newPaymentGroup().build()); + organizationMetadataById.put(orgId, metadata); + return metadata; + } + + private Map orgRegistrationById = new HashMap<>(); + + public OrganizationRegistration.Builder registerOrganization(String orgId) { + OrganizationRegistration.Builder registration = OrganizationRegistration.newBuilder() + .setOrgId(orgId); + orgRegistrationById.put(orgId, registration); + return registration; + } + + private void registerOrganizations() { + for (Map.Entry entry : orgRegistrationById.entrySet()) { + String orgId = entry.getKey(); + OrganizationRegistration.Builder registration = entry.getValue(); + OrganizationMetadata.Builder org = organizationMetadataById.get(orgId); + + URI orgMetadataUri = ipfs.addOrganization(org.build()); + registration.setMetadataUri(orgMetadataUri); + registry.addOrganizationRegistration(orgId, registration.build()); + } + } + + public Pricing.Builder newPricing() { + return Pricing.newBuilder() + .setPriceModel(PriceModel.FIXED_PRICE) + .setPriceInCogs(BigInteger.valueOf(11)); + } + + public EndpointGroup.Builder newEndpointGroup(String orgId) { + OrganizationMetadata.Builder org = organizationMetadataById.get(orgId); + byte[] paymentGroupId = org.build().getPaymentGroups().get(0).getPaymentGroupId(); + return EndpointGroup.newBuilder() + .setGroupName("default_group") + .addPricing(newPricing().build()) + .addEndpoint(server.getEndpoint()) + .setPaymentGroupId(paymentGroupId); + } + + private Map serviceMetadataById = new HashMap<>(); + + public ServiceMetadata.Builder newServiceMetadata(String serviceId, String orgId) { + ServiceMetadata.Builder metadata = ServiceMetadata.newBuilder() + .setDisplayName("Test Service Name") + .setMpeAddress(mpeAddress) + .addEndpointGroup(newEndpointGroup(orgId).build()); + serviceMetadataById.put(serviceId, metadata); + return metadata; + } + + private Map serviceRegistrationById = new HashMap<>(); + + public ServiceRegistration.Builder registerService(String orgId, String serviceId) { + ServiceRegistration.Builder serviceRegistration = ServiceRegistration.newBuilder() + .setServiceId(serviceId); + serviceRegistrationById.put(orgId + ":" + serviceId, serviceRegistration); + return serviceRegistration; + } + + private void registerServices() { for (Map.Entry entry : serviceRegistrationById.entrySet()) { + String orgIdserviceId = entry.getKey(); + int delimiter = orgIdserviceId.indexOf(":"); + String orgId = orgIdserviceId.substring(0, delimiter); + String serviceId = orgIdserviceId.substring(delimiter + 1); + ServiceRegistration.Builder registration = entry.getValue(); + ServiceMetadata.Builder metadata = serviceMetadataById.get(serviceId); + + OrganizationRegistration.Builder orgReg = orgRegistrationById.get(orgId); + orgReg.addServiceId(serviceId); + + URI serviceMetadataUri = ipfs.addService(metadata.build()); + registration.setMetadataUri(serviceMetadataUri); + registry.addServiceRegistration(orgId, serviceId, registration.build()); + } + } + + public PaymentChannel.Builder newPaymentChannel(byte[] groupId, Signer signer) { + BigInteger channelId = BigInteger.valueOf((long)(Math.random() * 100)); + PaymentGroup group = paymentGroupById.get(Utils.bytesToBase64(groupId)).build(); + PaymentChannel.Builder paymentChannel = PaymentChannel.newBuilder() + .setChannelId(channelId) + .setMpeContractAddress(mpeAddress) + .setNonce(BigInteger.valueOf(7)) + .setSender(new Address("0xC4f3BFE7D69461B7f363509393D44357c084404c")) + .setSigner(signer.getAddress()) + .setRecipient(group.getPaymentDetails().getPaymentAddress()) + .setPaymentGroupId(group.getPaymentGroupId()) + .setValue(BigInteger.valueOf(41)) + .setExpiration(BigInteger.valueOf(125)) + .setSpentAmount(BigInteger.valueOf(0)); + mpe.addPaymentChannel(paymentChannel.build()); + return paymentChannel; + } + + public EscrowPayment.Builder newEscrowPayment(PaymentChannel paymentChannel) { + return EscrowPayment.newBuilder() + .setPaymentChannel(paymentChannel) + .setAmount(BigInteger.valueOf(11)) + .setSigner(signerByAddress.get(paymentChannel.getSigner())); + } + + public PaymentChannelStateReply.Builder newPaymentChannelStateReply(EscrowPayment payment) { + return PaymentChannelStateReply.newBuilder() + .setCurrentNonce(payment.getChannelNonce()) + .setCurrentSignedAmount(payment.getAmount()) + .setCurrentSignature(payment.getSignature()); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/test/GrpcTest.java b/src/test/java/io/singularitynet/sdk/test/GrpcTest.java new file mode 100644 index 00000000..7c6f01c5 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/test/GrpcTest.java @@ -0,0 +1,61 @@ +package io.singularitynet.sdk.test; + +import org.junit.*; +import org.mockito.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.net.URL; +import io.grpc.*; + +import io.singularitynet.sdk.test.TestServiceGrpc.TestServiceBlockingStub; + +public class GrpcTest { + + private TestServer server; + private TestClientInterceptor clientInterceptor; + private ManagedChannel channel; + private TestServiceBlockingStub client; + + @Before + public void setUp() { + server = TestServer.startWithoutDaemon(); + URL url = server.getEndpoint(); + clientInterceptor = new TestClientInterceptor(); + channel = ManagedChannelBuilder + .forAddress(url.getHost(), url.getPort()) + .intercept(clientInterceptor) + .usePlaintext() + .build(); + client = TestServiceGrpc.newBlockingStub(channel); + } + + @After + public void tearDown() { + channel.shutdownNow(); + server.shutdownNow(); + } + + @Test + public void getServiceNameReturnsServiceName() { + client.echo(Input.newBuilder().setInput("ping").build()); + + assertEquals("Service name returned", "io.singularitynet.sdk.test.TestService", + MethodDescriptor.extractFullServiceName(clientInterceptor.method.getFullMethodName())); + } + + private static class TestClientInterceptor implements ClientInterceptor { + + private volatile MethodDescriptor method; + + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + this.method = method; + return next.newCall(method, callOptions); + } + + } +} diff --git a/src/test/java/io/singularitynet/sdk/test/GsonTest.java b/src/test/java/io/singularitynet/sdk/test/GsonTest.java index a464fa14..8e3bca53 100644 --- a/src/test/java/io/singularitynet/sdk/test/GsonTest.java +++ b/src/test/java/io/singularitynet/sdk/test/GsonTest.java @@ -2,7 +2,11 @@ import org.junit.*; import static org.junit.Assert.*; + +import java.lang.reflect.Type; +import java.util.Map; import com.google.gson.*; +import com.google.gson.reflect.TypeToken; public class GsonTest { @@ -16,6 +20,7 @@ public void setUp() { @Test public void testParseStringField() { StringField result = this.gson.fromJson("{ \"field\": \"value\" }", StringField.class); + assertEquals("value", result.field); } @@ -26,7 +31,9 @@ private static class StringField { @Test public void testParseCamelCaseField() { Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + CamelCaseField result = gson.fromJson("{ \"camel_case_field\": \"value\" }", CamelCaseField.class); + assertEquals("value", result.camelCaseField); } @@ -37,6 +44,7 @@ private static class CamelCaseField { @Test public void testParseFinalField() { FinalField result = this.gson.fromJson("{ \"field\": \"value\" }", FinalField.class); + assertEquals("value", result.field); } @@ -46,4 +54,32 @@ private FinalField() { this.field = ""; } } + + @Test + public void testReadToMap() { + String json = "{\"1\":{\"events\":{},\"links\":{},\"address\":\"0xdce9c76ccb881af94f7fb4fac94e4acc584fa9a5\",\"transactionHash\":\"0x29f3271851bb6b2a0d85fa94084945859467c872a00d158ab05dbd8c131c0e24\"},\"3\":{\"events\":{},\"links\":{},\"address\":\"0x663422c6999ff94933dbcb388623952cf2407f6f\",\"transactionHash\":\"0x150f6f8d47978152c3d1ed84f49d78aa4619a0b08c381b549f9ba6dedc818968\"},\"42\":{\"events\":{},\"links\":{},\"address\":\"0x89a780619a7b0542b52bbb929bc1ea01516542ec\",\"transactionHash\":\"0x5ba7650968492c2822175e80e4ceed9e86a50942ef8c243f6cd35b5d753b0add\"}}"; + Type mapType = new TypeToken>>(){}.getType(); + + Map> map = gson.fromJson(json, mapType); + + assertEquals("Kovan address", "0x89a780619a7b0542b52bbb929bc1ea01516542ec", map.get("42").get("address")); + } + + @Test + public void testParseEnumField() { + EnumField result = this.gson.fromJson("{ \"field\": \"FIRST\" }", EnumField.class); + + assertEquals(EnumField.EnumType.FIRST, result.field); + } + + private static class EnumField { + + EnumType field; + + private static enum EnumType { + FIRST, + SECOND + } + } + } diff --git a/src/test/java/io/singularitynet/sdk/test/IntegrationTest.java b/src/test/java/io/singularitynet/sdk/test/IntegrationTest.java deleted file mode 100644 index 5a55b52b..00000000 --- a/src/test/java/io/singularitynet/sdk/test/IntegrationTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.singularitynet.sdk.test; - -import org.junit.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -import io.grpc.Server; -import io.ipfs.api.IPFS; - -import io.singularitynet.sdk.contracts.*; -import io.singularitynet.sdk.client.*; -import io.singularitynet.sdk.registry.*; -import io.singularitynet.sdk.test.TestServiceGrpc.TestServiceBlockingStub; -import static io.singularitynet.sdk.registry.Utils.*; -import static io.singularitynet.sdk.registry.RegistryMock.*; -import static io.singularitynet.sdk.registry.IpfsMock.*; - -public class IntegrationTest { - - private Server testServer; - private RegistryMock registry; - private IpfsMock ipfs; - - private ServiceClient client; - - @Before - public void setUp() { - testServer = TestService.start(TestService.RANDOM_AVAILABLE_PORT); - registry = new RegistryMock(); - ipfs = new IpfsMock(); - - RegistryContract registryContract = new RegistryContract(registry.get()); - MetadataStorage metadataStorage = new IpfsMetadataStorage(ipfs.get()); - MetadataProvider metadataProvider = new RegistryMetadataProvider( - "test-org-id", "test-service-id", registryContract, metadataStorage); - client = new BaseServiceClient(metadataProvider); - } - - @After - public void tearDown() { - client.shutdownNow(); - testServer.shutdownNow(); - } - - @Test - public void clientCanCallGrpcServiceUsingSnetSdkGrpcChannel() { - registry.getServiceRegistrationById("test-org-id", "test-service-id") - .returns(serviceRegistration() - .setId("test-service-id") - .setMetadataUri("ipfs://QmR3anSdm4s13iLt3zzyrSbtvCDJNwhkrYG6yFGFHXBznb")); - ipfs.cat("QmR3anSdm4s13iLt3zzyrSbtvCDJNwhkrYG6yFGFHXBznb") - .returns(serviceMetadataJson(testServer.getPort())); - TestServiceBlockingStub stub = client.getGrpcStub(TestServiceGrpc::newBlockingStub); - - Output output = stub.echo(Input.newBuilder().setInput("ping").build()); - - assertEquals(Output.newBuilder().setOutput("ping").build(), output); - } - -} diff --git a/src/test/java/io/singularitynet/sdk/test/IpfsTest.java b/src/test/java/io/singularitynet/sdk/test/IpfsTest.java index 7e7800d2..e61f9991 100644 --- a/src/test/java/io/singularitynet/sdk/test/IpfsTest.java +++ b/src/test/java/io/singularitynet/sdk/test/IpfsTest.java @@ -3,9 +3,11 @@ import org.junit.*; import static org.junit.Assert.*; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URI; import io.ipfs.api.*; import io.ipfs.multihash.*; -import java.io.IOException; public class IpfsTest { @@ -16,4 +18,11 @@ public void loadFileFromSingularityNetIpfs() throws IOException { byte[] fileContents = ipfs.cat(filePointer); assertTrue("Non empty file from IPFS or exception is expected", fileContents.length > 0); } + + @Test + public void newIpfsUri() throws Exception { + URI uri = new URI("ipfs://Qma2KoWcf7f3c1m9nbr27LoPPHGonBBaTZeuxJ9L48CLS1"); + + assertEquals("URI authority", "Qma2KoWcf7f3c1m9nbr27LoPPHGonBBaTZeuxJ9L48CLS1", uri.getAuthority()); + } } diff --git a/src/test/java/io/singularitynet/sdk/test/SingleServiceSingleClientTest.java b/src/test/java/io/singularitynet/sdk/test/SingleServiceSingleClientTest.java new file mode 100644 index 00000000..0d656df9 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/test/SingleServiceSingleClientTest.java @@ -0,0 +1,113 @@ +package io.singularitynet.sdk.test; + +import org.junit.*; +import static org.junit.Assert.*; + +import java.math.BigInteger; + +import io.singularitynet.sdk.registry.*; +import io.singularitynet.sdk.mpe.*; +import io.singularitynet.sdk.client.*; +import io.singularitynet.sdk.ethereum.*; +import io.singularitynet.sdk.daemon.*; +import io.singularitynet.sdk.test.TestServiceGrpc.TestServiceBlockingStub; + +public class SingleServiceSingleClientTest { + + private Environment env; + + private BigInteger price; + private PaymentChannel paymentChannel; + + private ServiceClient serviceClient; + private TestServiceBlockingStub serviceStub; + + @Before + public void setUp() throws Exception { + env = Environment.env(); + + + String orgId = "test-org-id"; + env.newOrganizationMetadata(orgId); + env.registerOrganization(orgId); + + String serviceId = "test-service-id"; + ServiceMetadata.Builder service = env.newServiceMetadata(serviceId, orgId); + price = BigInteger.valueOf(11); + EndpointGroup endpointGroup = env.newEndpointGroup(orgId) + .clearPricing() + .addPricing(env.newPricing().setPriceInCogs(price).build()) + .build(); + service.clearEndpointGroups().addEndpointGroup(endpointGroup); + env.registerService(orgId, serviceId); + + Signer signer = env.newSigner(); + + paymentChannel = env.newPaymentChannel(endpointGroup.getPaymentGroupId(), signer).build(); + env.daemon().setChannelStateIsAbsent(paymentChannel); + + env.updateMocks(); + + RegistryContract registryContract = new RegistryContract(env.registry().get()); + MetadataStorage metadataStorage = new IpfsMetadataStorage(env.ipfs().get()); + MetadataProvider metadataProvider = new RegistryMetadataProvider( + orgId, serviceId, registryContract, metadataStorage); + MultiPartyEscrowContract mpeContract = new MultiPartyEscrowContract(env.mpe().get()); + DaemonConnection connection = new FirstEndpointDaemonConnection( + endpointGroup.getGroupName(), metadataProvider); + PaymentChannelStateService stateService = new PaymentChannelStateService( + connection, mpeContract, env.ethereum(), signer); + PaymentChannelProvider paymentChannelProvider = + new AskDaemonFirstPaymentChannelProvider(mpeContract, stateService); + PaymentStrategy paymentStrategy = new FixedPaymentChannelPaymentStrategy( + paymentChannel.getChannelId()); + serviceClient = new BaseServiceClient(connection, metadataProvider, + paymentChannelProvider, paymentStrategy, signer); + + serviceStub = serviceClient.getGrpcStub(TestServiceGrpc::newBlockingStub); + } + + @After + public void tearDown() { + serviceClient.shutdownNow(); + env.server().shutdownNow(); + } + + @Test + public void clientCanCallGrpcServiceUsingSnetSdkGrpcChannel() { + Output output = serviceStub.echo(Input.newBuilder().setInput("ping").build()); + + assertEquals("Result returned", Output.newBuilder().setOutput("ping").build(), output); + } + + @Test + public void clientSendsPaymentDataInGrpcMetadata() { + Output output = serviceStub.echo(Input.newBuilder().setInput("ping").build()); + + assertEquals("Number of payments received by daemon", 1, env.daemon().getPayments().size()); + } + + @Test + public void sendPaymentForChannelNotUsedBefore() { + Output output = serviceStub.echo(Input.newBuilder().setInput("ping").build()); + + EscrowPayment expectedPayment = env.newEscrowPayment(paymentChannel).setAmount(price).build(); + assertEquals("Payment received by daemon", expectedPayment, env.daemon().getPayments().get(0)); + } + + @Test + public void sendPaymentForChannelUsedBeforeNoClaim() { + BigInteger prevPrice = BigInteger.valueOf(3); + EscrowPayment prevPayment = env.newEscrowPayment(paymentChannel) + .setAmount(prevPrice).build(); + env.daemon().setChannelState(paymentChannel.getChannelId(), + env.newPaymentChannelStateReply(prevPayment).build()); + + Output output = serviceStub.echo(Input.newBuilder().setInput("ping").build()); + + EscrowPayment expectedPayment = env.newEscrowPayment(paymentChannel) + .setAmount(prevPrice.add(price)).build(); + assertEquals("Payment received by daemon", expectedPayment, env.daemon().getPayments().get(0)); + } + +} diff --git a/src/test/java/io/singularitynet/sdk/test/TestServer.java b/src/test/java/io/singularitynet/sdk/test/TestServer.java new file mode 100644 index 00000000..42ddad50 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/test/TestServer.java @@ -0,0 +1,74 @@ +package io.singularitynet.sdk.test; + +import io.grpc.stub.StreamObserver; +import io.grpc.*; +import java.io.IOException; +import java.net.URL; +import java.util.Optional; + +import io.singularitynet.sdk.daemon.DaemonMock; + +public class TestServer { + + private static final int RANDOM_AVAILABLE_PORT = 0; + + private final Server server; + private final TestService testService; + + private TestServer(Server server, TestService testService) { + this.server = server; + this.testService = testService; + } + + public static TestServer start(DaemonMock daemon) { + return startInternal(Optional.of(daemon)); + } + + public static TestServer startWithoutDaemon() { + return startInternal(Optional.empty()); + } + + private static TestServer startInternal(Optional daemon) { + TestService service = new TestService(); + + ServerBuilder builder = ServerBuilder + .forPort(RANDOM_AVAILABLE_PORT) + .addService(service); + if (daemon.isPresent()) { + builder.addService(daemon.get()).intercept(daemon.get()); + } + Server server = builder.build(); + try { + server.start(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return new TestServer(server, service); + } + + public void shutdownNow() { + server.shutdownNow(); + } + + public URL getEndpoint() { + try { + return new URL("http://localhost:" + String.valueOf(server.getPort())); + } catch (java.net.MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public static class TestService extends TestServiceGrpc.TestServiceImplBase { + + @Override + public void echo(Input input, StreamObserver callback) { + Output output = Output.newBuilder() + .setOutput(input.getInput()) + .build(); + callback.onNext(output); + callback.onCompleted(); + } + + } + +} diff --git a/src/test/java/io/singularitynet/sdk/test/TestService.java b/src/test/java/io/singularitynet/sdk/test/TestService.java deleted file mode 100644 index e8b2f6c9..00000000 --- a/src/test/java/io/singularitynet/sdk/test/TestService.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.singularitynet.sdk.test; - -import io.grpc.stub.StreamObserver; -import io.grpc.ServerBuilder; -import io.grpc.Server; -import java.io.IOException; - -public class TestService extends TestServiceGrpc.TestServiceImplBase { - - public static final int RANDOM_AVAILABLE_PORT = 0; - - public static Server start(int port) { - Server server = ServerBuilder - .forPort(port) - .addService(new TestService()) - .build(); - try { - server.start(); - } catch (IOException e) { - throw new RuntimeException(e); - } - return server; - } - - public void echo(Input input, StreamObserver callback) { - Output output = Output.newBuilder() - .setOutput(input.getInput()) - .build(); - callback.onNext(output); - callback.onCompleted(); - } - -} diff --git a/src/test/java/io/singularitynet/sdk/test/Web3jTest.java b/src/test/java/io/singularitynet/sdk/test/Web3jTest.java new file mode 100644 index 00000000..4392de66 --- /dev/null +++ b/src/test/java/io/singularitynet/sdk/test/Web3jTest.java @@ -0,0 +1,52 @@ +package io.singularitynet.sdk.test; + +import org.junit.*; +import static org.junit.Assert.*; + +import java.util.List; +import java.math.BigInteger; +import java.io.IOException; +import org.web3j.tx.ReadonlyTransactionManager; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.http.HttpService; +import org.web3j.tx.gas.ContractGasProvider; +import org.web3j.tx.gas.DefaultGasProvider; +import org.web3j.tuples.generated.*; + +import io.singularitynet.sdk.contracts.Registry; +import io.singularitynet.sdk.common.Utils; + +public class Web3jTest { + + private Web3j web3j; + private ReadonlyTransactionManager roTransactionManager; + private DefaultGasProvider gasProvider; + + @Before + public void setUp() { + web3j = Web3j.build(new HttpService("https://ropsten.infura.io")); + roTransactionManager = new ReadonlyTransactionManager( + web3j, "0x008f312C5635a66c0fB49952D7C431D765bb3D3c"); + gasProvider = new DefaultGasProvider(); + } + + @Test + public void getServiceRegistrationById() throws Exception { + Registry registry = Registry.load("0x663422c6999Ff94933DBCb388623952CF2407F6f", web3j, + roTransactionManager, gasProvider); + + Tuple4> result = + registry.getServiceRegistrationById(Utils.strToBytes32("snet"), + Utils.strToBytes32("speech-recognition")).send(); + + assertTrue(result.component1()); + } + + @Test + public void ethBlockNumber() throws IOException { + BigInteger blockNumber = web3j.ethBlockNumber().send().getBlockNumber(); + + assertNotNull(blockNumber); + } + +}