From 33b402c4b2e328e314764074ba36624e6bd4ba6f Mon Sep 17 00:00:00 2001 From: Kodai Doki <52027276+KodaiD@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:27:43 +0900 Subject: [PATCH] Update simple bank account app (#61) --- .../simple-bank-account/README.md | 76 ++--- .../simple-bank-account/app/build.gradle | 53 +++ .../application/bankaccount/Application.java | 0 .../controller/AccountController.java | 0 .../controller/TransferController.java | 0 .../exception/ResourceNotFoundException.java | 0 .../bankaccount/model/Account.java | 0 .../repository/AccountRepository.java | 16 +- .../bankaccount/service/AccountService.java | 2 +- .../src/main/resources/application.properties | 2 +- .../src/main/resources/log4j.properties | 0 .../bankaccount/model/AccountTest.java | 8 +- .../simple-bank-account/build.gradle | 40 --- .../conf/client.properties | 4 +- .../simple-bank-account/contract/build.gradle | 30 ++ .../simple-bank-account/contract/register | 4 + .../bankaccount/contract/AccountHistory.java | 51 +++ .../bankaccount/contract/CreateAccount.java | 31 ++ .../bankaccount/contract/Deposit.java | 42 +++ .../bankaccount/contract/Transfer.java | 57 ++++ .../bankaccount/contract/Withdraw.java | 46 +++ .../contract/AccountHistoryTest.java | 84 ++--- .../contract/CreateAccountTest.java | 46 +-- .../bankaccount/contract/DepositTest.java | 64 ++-- .../bankaccount/contract/TransferTest.java | 114 ++++--- .../bankaccount/contract/WithdrawTest.java | 67 ++-- .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- docs/applications/simple-bank-account/gradlew | 301 +++++++++++------- .../simple-bank-account/gradlew.bat | 76 +++-- .../applications/simple-bank-account/register | 4 - .../simple-bank-account/settings.gradle | 2 + .../bankaccount/contract/AccountHistory.java | 54 ---- .../bankaccount/contract/CreateAccount.java | 31 -- .../bankaccount/contract/Deposit.java | 41 --- .../bankaccount/contract/Transfer.java | 58 ---- .../bankaccount/contract/Withdraw.java | 45 --- 37 files changed, 818 insertions(+), 635 deletions(-) create mode 100644 docs/applications/simple-bank-account/app/build.gradle rename docs/applications/simple-bank-account/{ => app}/src/main/java/com/scalar/application/bankaccount/Application.java (100%) rename docs/applications/simple-bank-account/{ => app}/src/main/java/com/scalar/application/bankaccount/controller/AccountController.java (100%) rename docs/applications/simple-bank-account/{ => app}/src/main/java/com/scalar/application/bankaccount/controller/TransferController.java (100%) rename docs/applications/simple-bank-account/{ => app}/src/main/java/com/scalar/application/bankaccount/exception/ResourceNotFoundException.java (100%) rename docs/applications/simple-bank-account/{ => app}/src/main/java/com/scalar/application/bankaccount/model/Account.java (100%) rename docs/applications/simple-bank-account/{ => app}/src/main/java/com/scalar/application/bankaccount/repository/AccountRepository.java (80%) rename docs/applications/simple-bank-account/{ => app}/src/main/java/com/scalar/application/bankaccount/service/AccountService.java (96%) rename docs/applications/simple-bank-account/{ => app}/src/main/resources/application.properties (79%) rename docs/applications/simple-bank-account/{ => app}/src/main/resources/log4j.properties (100%) rename docs/applications/simple-bank-account/{ => app}/src/test/java/com/scalar/application/bankaccount/model/AccountTest.java (84%) delete mode 100644 docs/applications/simple-bank-account/build.gradle create mode 100644 docs/applications/simple-bank-account/contract/build.gradle create mode 100755 docs/applications/simple-bank-account/contract/register create mode 100644 docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/AccountHistory.java create mode 100644 docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/CreateAccount.java create mode 100644 docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Deposit.java create mode 100644 docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Transfer.java create mode 100644 docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Withdraw.java rename docs/applications/simple-bank-account/{ => contract}/src/test/java/com/scalar/application/bankaccount/contract/AccountHistoryTest.java (59%) rename docs/applications/simple-bank-account/{ => contract}/src/test/java/com/scalar/application/bankaccount/contract/CreateAccountTest.java (50%) rename docs/applications/simple-bank-account/{ => contract}/src/test/java/com/scalar/application/bankaccount/contract/DepositTest.java (53%) rename docs/applications/simple-bank-account/{ => contract}/src/test/java/com/scalar/application/bankaccount/contract/TransferTest.java (52%) rename docs/applications/simple-bank-account/{ => contract}/src/test/java/com/scalar/application/bankaccount/contract/WithdrawTest.java (54%) create mode 100644 docs/applications/simple-bank-account/gradle/wrapper/gradle-wrapper.jar delete mode 100755 docs/applications/simple-bank-account/register delete mode 100644 docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/AccountHistory.java delete mode 100644 docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/CreateAccount.java delete mode 100644 docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Deposit.java delete mode 100644 docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Transfer.java delete mode 100644 docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Withdraw.java diff --git a/docs/applications/simple-bank-account/README.md b/docs/applications/simple-bank-account/README.md index 36cfc9f1..81548e34 100644 --- a/docs/applications/simple-bank-account/README.md +++ b/docs/applications/simple-bank-account/README.md @@ -20,7 +20,7 @@ This application uses five contracts: - `Transfer.java` - `Withdraw.java` -(which can be found in [`src/main/java/com/scalar/application/bankaccount/contract`](./src/main/java/com/scalar/application/bankaccount/contract)). These contracts will be registered by the bank and will allow the bank to, respectively, view account histories, create accounts, deposit funds to an account, transfer funds between accounts, and withdraw funds from accounts. +(which can be found in [`contract/src/main/java/com/scalar/application/bankaccount/contract`](./contract/src/main/java/com/scalar/application/bankaccount/contract)). These contracts will be registered by the bank and will allow the bank to, respectively, view account histories, create accounts, deposit funds to an account, transfer funds between accounts, and withdraw funds from accounts. The overall architecture of this application can be viewed as follows. (Note again that this use case is for simplicity, and in practice may look a bit different.) @@ -31,6 +31,8 @@ The overall architecture of this application can be viewed as follows. (Note aga Download the [ScalarDL Client SDK](https://github.com/scalar-labs/scalardl-client-sdk). Make sure ScalarDL is running and register all the required contracts by executing ``` +$ ./gradlew build +$ cd contract $ SCALAR_SDK_HOME=/path/to/scalardl-client-sdk ./register ``` Run the application using IntelliJ (or the IDE of your choice), or by executing `gradle bootRun` in the project home directory. It should create a server on `localhost:8080` to which you can send HTTP requests in order to interact with the app. See the [API documentation](./docs/api_endpoints.md) for more information. To create HTTP requests we have found that [Postman](https://www.getpostman.com/) is quite nice. @@ -43,50 +45,52 @@ In this tutorial we will not discuss the detail at the level of web services or ### Contracts -Contracts are Java classes which extend the `Contract` class and override the `invoke` method. Let's take a closer look at the `Deposit.java` contract. +Contracts are Java classes which extend the `JacksonBasedContract` class and override the `invoke` method. Let's take a closer look at the `Deposit.java` contract. ```java package com.scalar.application.bankaccount.contract; -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; +import com.fasterxml.jackson.databind.JsonNode; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; +import com.scalar.dl.ledger.statemachine.Ledger; import java.util.Optional; -import javax.json.Json; -import javax.json.JsonObject; +import javax.annotation.Nullable; -public class Deposit extends Contract { +public class Deposit extends JacksonBasedContract { @Override - public JsonObject invoke(Ledger ledger, JsonObject argument, Optional property) { - if (!(argument.containsKey("id") && argument.containsKey("amount"))) { + public JsonNode invoke( + Ledger ledger, JsonNode argument, @Nullable JsonNode properties) { + if (!argument.has("id") || !argument.has("amount")) { throw new ContractContextException("a required key is missing: id and/or amount"); } - String id = argument.getString("id"); - long amount = argument.getJsonNumber("amount").longValue(); + String id = argument.get("id").asText(); + long amount = argument.get("amount").asLong(); if (amount < 0) { throw new ContractContextException("amount is negative"); } - Optional response = ledger.get(id); + Optional> asset = ledger.get(id); - if (!response.isPresent()) { + if (!asset.isPresent()) { throw new ContractContextException("account does not exist"); } - long oldBalance = response.get().data().getInt("balance"); + long oldBalance = asset.get().data().get("balance").asLong(); long newBalance = oldBalance + amount; - ledger.put(id, Json.createObjectBuilder().add("balance", newBalance).build()); - return Json.createObjectBuilder() - .add("status", "succeeded") - .add("old_balance", oldBalance) - .add("new_balance", newBalance) - .build(); + ledger.put(id, getObjectMapper().createObjectNode().put("balance", newBalance)); + return getObjectMapper() + .createObjectNode() + .put("status", "succeeded") + .put("old_balance", oldBalance) + .put("new_balance", newBalance); } } + ``` In order for this contract to function properly the user must supply an account `id` and an `amount`. So the first thing to do is check whether the argument contains these two keys, and if not, throw a `ContractContextException`. @@ -95,15 +99,15 @@ In order for this contract to function properly the user must supply an account So, assuming that we have an `id` and an `amount`, we do a quick non-negative check on `amount` and again throw a `ContractContextException` if it is. Now we are ready to interact with the `ledger`. -There are three methods that can be called on `ledger`: `get(String s)`, `put(String s, JsonObject jsonObject)`, and `scan(AssetFilter assetFilter)`. `get(String s)` will retrieve the asset `s` from the ledger. `put(String s, JsonObject argument)` will associate the asset `s` with the data `jsonObject` and increase the age of the asset. `scan(AssetFilter assetFilter)` will return a version of the history of an asset as specified in the `AssetFilter`. +There are three methods that can be called on `ledger`: `get(String s)`, `put(String s, JsonNode jsonNode)`, and `scan(AssetFilter assetFilter)`. `get(String s)` will retrieve the asset `s` from the ledger. `put(String s, JsonNode jsonNode)` will associate the asset `s` with the data `jsonNode` and increase the age of the asset. `scan(AssetFilter assetFilter)` will return a version of the history of an asset as specified in the `AssetFilter`. **Note:** ledger does not permit blind writes, i.e., before performing a `put` on a particular asset, we must first `get` that asset. Furthermore `scan` is only allowed in read-only contracts, which means a single contract cannot both `scan` and `put`. The rest of the contract proceeds in a straightforward manner. We first `get` the asset from the ledger, retrieve its current balance, add the deposit amount to it, and finally `put` the asset back into the ledger with its new balance. -At the end we must return a `JsonObject`. What the `JsonObject` contains is up to the designer of the contract. Here we have decided to include a `status` message, the `old_balance`, and the `new_balance`. +At the end we must return a `JsonNode`. What the `JsonNode` contains is up to the designer of the contract. Here we have decided to include a `status` message, the `old_balance`, and the `new_balance`. -If you wish, you can view the other contracts that this application uses in [`scr/main/java/com/scalar/application/bankaccount/contract`](./src/main/java/com/scalar/application/bankaccount/contract). +If you wish, you can view the other contracts that this application uses in [`contract/scr/main/java/com/scalar/application/bankaccount/contract`](./contract/src/main/java/com/scalar/application/bankaccount/contract). Once you have written your contracts you will need to compile them, and this can be done as @@ -128,7 +132,8 @@ scalar.dl.client.private_key_path=conf/client-key.pem If everything is set up properly you should be able to register your certificate on the ScalarDL network as ```bash -$ ${SCALAR_SDK_HOME}/client/bin/scalardl register-cert --properties ./conf/client.properties +$ cd contract +$ ${SCALAR_SDK_HOME}/client/bin/scalardl register-cert --properties ../conf/client.properties ``` You should receive status code 200 if successful. @@ -149,7 +154,7 @@ contract-class-file = "build/classes/java/main/com/scalar/application/bankaccoun [[contracts]] contract-id = "transfer" contract-binary-name = "com.scalar.application.bankaccount.contract.Transfer" -contract-class-file = "build/classes/java/main/com/scalar/application/bankaccount/contract/Transfer.class" +contract-class-file = "build/classes/java/main/com/scalar/application/bankaccount/contract/Transfer.class" ``` In this example we will register three contracts: `CreateAccount.java`, `Deposit.java`, and `Transfer.java`. The `contract-binary-name` and `contract-class-file` are determined, but you are free to choose the `contract-id` as you wish. The `contract-id` is how you can refer to a specific contract using `ClientService`, as we will see below. @@ -157,7 +162,7 @@ In this example we will register three contracts: `CreateAccount.java`, `Deposit Once your toml file is written you can register all the specified contracts as ```bash -$ ${SCALAR_SDK_HOME}/client/bin/scalardl register-contracts --properties ./conf/client.properties --contracts-file ./conf/contracts.toml +$ ${SCALAR_SDK_HOME}/client/bin/scalardl register-contracts --properties ../conf/client.properties --contracts-file ../conf/contracts.toml ``` Each successfully registered contract should return status code 200. @@ -169,20 +174,20 @@ You can now execute any registered contracts if you would like. For example, use Create two accounts with ids `a111` and `b222`. (Contract ids can be any string.) ```bash -$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ./conf/client.properties --contract-id create-account --contract-argument '{"id": "a111"}' -$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ./conf/client.properties --contract-id create-account --contract-argument '{"id": "b222"}' +$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ../conf/client.properties --contract-id create-account --contract-argument '{"id": "a111"}' +$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ../conf/client.properties --contract-id create-account --contract-argument '{"id": "b222"}' ``` Now, deposit 100 into account `a111`: ```bash -$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ./conf/client.properties --contract-id deposit --contract-argument '{"id": "a111", "amount": 100}' +$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ../conf/client.properties --contract-id deposit --contract-argument '{"id": "a111", "amount": 100}' ``` Finally, transfer 25 from `a111` to `b222`: ```bash -$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ./conf/client.properties --contract-id transfer --contract-argument '{"from": "a111", "to": "b222", "amount": 100}' +$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ../conf/client.properties --contract-id transfer --contract-argument '{"from": "a111", "to": "b222", "amount": 100}' ``` If you were running the application itself, you could execute these commands using the [API endpoints](./docs/api_endpoints.md). @@ -195,18 +200,15 @@ The Client SDK is available on [Maven Central](https://search.maven.org/search?q ```groovy dependencies { - compile group: 'com.scalar-labs', name: 'scalardl-java-client-sdk', version: '2.0.4' + compile group: 'com.scalar-labs', name: 'scalardl-java-client-sdk', version: '3.9.1' } ``` The following snippet shows how you can instantiate a `ClientService` object, where `properties` should be the path to your `client.properties` file. ```java -Injector injector = - Guice.createInjector(new ClientModule(new ClientConfig(new File(properties)))); -try (ClientService clientService = injector.getInstance(ClientService.class)) { - ... -} +ClientServiceFactory factory = new ClientServiceFactory(); +ClientService service = factory.create(new ClientConfig(new File(properties)); ``` `ClientService` contains a method `executeContract(String id, JsonObject argument)` which can be used to, of course, execute a contract. For example: diff --git a/docs/applications/simple-bank-account/app/build.gradle b/docs/applications/simple-bank-account/app/build.gradle new file mode 100644 index 00000000..94e76fe9 --- /dev/null +++ b/docs/applications/simple-bank-account/app/build.gradle @@ -0,0 +1,53 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:3.3.0") + } +} + +plugins { + id 'java' + id 'application' + id 'idea' + id "org.springframework.boot" version "3.3.0" + id "io.spring.dependency-management" version "1.1.5" +} + +application { + mainClass = 'com.scalar.application.bankaccount.Application' +} + +bootJar { + archiveBaseName = 'gs-rest-service' + archiveVersion = '0.1.0' +} + +repositories { + mavenCentral() +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +group = 'com.scalar.application.simple-bank-account' +version = '0.1' + +dependencies { + implementation('org.springframework.boot:spring-boot-starter-web') { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' + } + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + implementation group: 'com.scalar-labs', name: 'scalardl-java-client-sdk', version: '3.9.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.assertj:assertj-core:3.26.0' +} diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/Application.java b/docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/Application.java similarity index 100% rename from docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/Application.java rename to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/Application.java diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/controller/AccountController.java b/docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/controller/AccountController.java similarity index 100% rename from docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/controller/AccountController.java rename to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/controller/AccountController.java diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/controller/TransferController.java b/docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/controller/TransferController.java similarity index 100% rename from docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/controller/TransferController.java rename to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/controller/TransferController.java diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/exception/ResourceNotFoundException.java b/docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/exception/ResourceNotFoundException.java similarity index 100% rename from docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/exception/ResourceNotFoundException.java rename to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/exception/ResourceNotFoundException.java diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/model/Account.java b/docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/model/Account.java similarity index 100% rename from docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/model/Account.java rename to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/model/Account.java diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/repository/AccountRepository.java b/docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/repository/AccountRepository.java similarity index 80% rename from docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/repository/AccountRepository.java rename to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/repository/AccountRepository.java index bbca41a3..ccec1800 100644 --- a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/repository/AccountRepository.java +++ b/docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/repository/AccountRepository.java @@ -1,14 +1,11 @@ package com.scalar.application.bankaccount.repository; -import com.google.inject.Guice; -import com.google.inject.Injector; import com.scalar.dl.client.config.ClientConfig; -import com.scalar.dl.client.service.ClientModule; import com.scalar.dl.client.service.ClientService; +import com.scalar.dl.client.service.ClientServiceFactory; import com.scalar.dl.ledger.model.ContractExecutionResult; import java.io.File; import java.io.IOException; -import javax.inject.Singleton; import javax.json.JsonObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -16,7 +13,6 @@ import org.springframework.stereotype.Repository; @Repository -@Singleton public class AccountRepository { private static final Logger logger = LogManager.getLogger(AccountRepository.class); private static final String ACCOUNT_HISTORY_ID = "account-history"; @@ -28,9 +24,8 @@ public class AccountRepository { public AccountRepository(@Value("${client.properties.path}") String properties) throws IOException { - Injector injector = - Guice.createInjector(new ClientModule(new ClientConfig(new File(properties)))); - this.clientService = injector.getInstance(ClientService.class); + ClientServiceFactory clientServiceFactory = new ClientServiceFactory(); + this.clientService = clientServiceFactory.create(new ClientConfig(new File(properties))); } public ContractExecutionResult create(JsonObject argument) { @@ -40,8 +35,7 @@ public ContractExecutionResult create(JsonObject argument) { } public ContractExecutionResult history(JsonObject argument) { - ContractExecutionResult result = - clientService.executeContract(ACCOUNT_HISTORY_ID, argument); + ContractExecutionResult result = clientService.executeContract(ACCOUNT_HISTORY_ID, argument); logResponse("history", result); return result; } @@ -68,7 +62,7 @@ private void logResponse(String header, ContractExecutionResult result) { logger.info( header + ": (" - + (result.getResult().isPresent() ? result.getResult().get() : "{}") + + (result.getContractResult().isPresent() ? result.getContractResult().get() : "{}") + ")"); } } diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/service/AccountService.java b/docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/service/AccountService.java similarity index 96% rename from docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/service/AccountService.java rename to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/service/AccountService.java index b4cc30fc..99b4bacd 100644 --- a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/service/AccountService.java +++ b/docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/service/AccountService.java @@ -71,7 +71,7 @@ private ResponseEntity serve(ThrowableFunction f, JsonObject json) { ContractExecutionResult result = f.apply(json); return ResponseEntity - .ok(result.getResult().isPresent() ? result.getResult().get().toString() : "{}"); + .ok(result.getContractResult().isPresent() ? result.getContractResult().get() : "{}"); } catch (ClientException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Json.createObjectBuilder() diff --git a/docs/applications/simple-bank-account/src/main/resources/application.properties b/docs/applications/simple-bank-account/app/src/main/resources/application.properties similarity index 79% rename from docs/applications/simple-bank-account/src/main/resources/application.properties rename to docs/applications/simple-bank-account/app/src/main/resources/application.properties index 18e68b3b..7b786a6e 100644 --- a/docs/applications/simple-bank-account/src/main/resources/application.properties +++ b/docs/applications/simple-bank-account/app/src/main/resources/application.properties @@ -1,4 +1,4 @@ -client.properties.path=conf/client.properties +client.properties.path=../conf/client.properties # Contract ids contract.id.account-history=account-history diff --git a/docs/applications/simple-bank-account/src/main/resources/log4j.properties b/docs/applications/simple-bank-account/app/src/main/resources/log4j.properties similarity index 100% rename from docs/applications/simple-bank-account/src/main/resources/log4j.properties rename to docs/applications/simple-bank-account/app/src/main/resources/log4j.properties diff --git a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/model/AccountTest.java b/docs/applications/simple-bank-account/app/src/test/java/com/scalar/application/bankaccount/model/AccountTest.java similarity index 84% rename from docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/model/AccountTest.java rename to docs/applications/simple-bank-account/app/src/test/java/com/scalar/application/bankaccount/model/AccountTest.java index 840534be..83a7b88d 100644 --- a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/model/AccountTest.java +++ b/docs/applications/simple-bank-account/app/src/test/java/com/scalar/application/bankaccount/model/AccountTest.java @@ -1,17 +1,17 @@ package com.scalar.application.bankaccount.model; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Java6Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatCode; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class AccountTest { private static final String ACCOUNT_ID = "account-id"; private static final long BALANCE = 123; private Account account; - @Before + @BeforeEach public void setUp() { account = new Account(ACCOUNT_ID, BALANCE); } diff --git a/docs/applications/simple-bank-account/build.gradle b/docs/applications/simple-bank-account/build.gradle deleted file mode 100644 index f4b492a1..00000000 --- a/docs/applications/simple-bank-account/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") - } -} - -plugins { - id 'java' - id 'application' - id 'idea' - id "org.springframework.boot" version "2.1.1.RELEASE" - id "io.spring.dependency-management" version "1.0.6.RELEASE" -} - -bootJar { - baseName = 'gs-rest-service' - version = '0.1.0' -} - -repositories { - mavenCentral() -} - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 -group = 'com.scalar.application.simple-bank-account' -version = '0.1' - -dependencies { - compile('org.springframework.boot:spring-boot-starter-web') { - exclude module: 'logback-classic' - } - compile group: 'com.scalar-labs', name: 'scalardl-java-client-sdk', version: '2.0.4' - testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile group: 'junit', name: 'junit', version: '4.12' - testCompile 'org.assertj:assertj-core:3.9.1' -} diff --git a/docs/applications/simple-bank-account/conf/client.properties b/docs/applications/simple-bank-account/conf/client.properties index 521dc10a..7c8a5452 100644 --- a/docs/applications/simple-bank-account/conf/client.properties +++ b/docs/applications/simple-bank-account/conf/client.properties @@ -17,12 +17,12 @@ scalar.dl.client.cert_holder_id=user1 #scalar.dl.client.cert_version=1 # Required. The path of the certificate file. -scalar.dl.client.cert_path=./conf/client.pem +scalar.dl.client.cert_path=../conf/client.pem # Required. The path of a corresponding private key file to the certificate. # Exceptionally it can be empty in some requests to privileged services # such as registerCertificate and registerFunction since they don't need a signature. -scalar.dl.client.private_key_path=./conf/client-key.pem +scalar.dl.client.private_key_path=../conf/client-key.pem # Optional. A flag to enable TLS communication. False by default. scalar.dl.client.tls.enabled=false diff --git a/docs/applications/simple-bank-account/contract/build.gradle b/docs/applications/simple-bank-account/contract/build.gradle new file mode 100644 index 00000000..0f3300cf --- /dev/null +++ b/docs/applications/simple-bank-account/contract/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'idea' +} + +repositories { + mavenCentral() +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +group = 'com.scalar.application.simple-bank-account' +version = '0.1' + +dependencies { + implementation group: 'com.scalar-labs', name: 'scalardl-java-client-sdk', version: '3.9.1' + testImplementation group: 'com.scalar-labs', name: 'scalardl-java-client-sdk', version: '3.9.1' + testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.11.0' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.assertj:assertj-core:3.26.0' +} diff --git a/docs/applications/simple-bank-account/contract/register b/docs/applications/simple-bank-account/contract/register new file mode 100755 index 00000000..3de3d498 --- /dev/null +++ b/docs/applications/simple-bank-account/contract/register @@ -0,0 +1,4 @@ +#!/bin/bash + +${SCALAR_SDK_HOME}/client/bin/scalardl register-cert --properties ../conf/client.properties +${SCALAR_SDK_HOME}/client/bin/scalardl register-contracts --properties ../conf/client.properties --contracts-file ../conf/contracts.toml diff --git a/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/AccountHistory.java b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/AccountHistory.java new file mode 100644 index 00000000..2b8e2363 --- /dev/null +++ b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/AccountHistory.java @@ -0,0 +1,51 @@ +package com.scalar.application.bankaccount.contract; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.scalar.dl.ledger.database.AssetFilter; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; +import com.scalar.dl.ledger.exception.ContractContextException; +import com.scalar.dl.ledger.statemachine.Ledger; +import java.util.List; +import javax.annotation.Nullable; + +public class AccountHistory extends JacksonBasedContract { + @Override + public JsonNode invoke(Ledger ledger, JsonNode argument, @Nullable JsonNode property) { + if (!argument.has("id")) { + throw new ContractContextException("a required key is missing: id"); + } + + AssetFilter filter = new AssetFilter(argument.get("id").asText()); + if (argument.has("start")) { + filter.withStartAge(argument.get("start").asInt(), true); + } + if (argument.has("end")) { + filter.withEndAge(argument.get("end").asInt(), false); + } + if (argument.has("limit")) { + filter.withLimit(argument.get("limit").asInt()); + } + if (argument.has("order") && argument.get("order").asText().equals("asc")) { + filter.withAgeOrder(AssetFilter.AgeOrder.ASC); + } + + List> history = ledger.scan(filter); + + ArrayNode result = JsonNodeFactory.instance.arrayNode(); + history.forEach( + asset -> { + JsonNode json = + getObjectMapper() + .createObjectNode() + .put("id", asset.id()) + .put("data", asset.data().asText()) + .put("age", asset.age()); + result.add(json); + }); + + return getObjectMapper().createObjectNode().put("status", "succeeded").set("history", result); + } +} diff --git a/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/CreateAccount.java b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/CreateAccount.java new file mode 100644 index 00000000..a6dd72af --- /dev/null +++ b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/CreateAccount.java @@ -0,0 +1,31 @@ +package com.scalar.application.bankaccount.contract; + +import com.fasterxml.jackson.databind.JsonNode; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; +import com.scalar.dl.ledger.exception.ContractContextException; +import com.scalar.dl.ledger.statemachine.Ledger; +import java.util.Optional; +import javax.annotation.Nullable; + +public class CreateAccount extends JacksonBasedContract { + @Override + public JsonNode invoke(Ledger ledger, JsonNode argument, @Nullable JsonNode property) { + if (!argument.has("id")) { + throw new ContractContextException("a required key is missing: id"); + } + + String id = argument.get("id").asText(); + Optional> response = ledger.get(id); + + if (response.isPresent()) { + throw new ContractContextException("account already exists"); + } + + ledger.put(id, getObjectMapper().createObjectNode().put("balance", 0)); + return getObjectMapper() + .createObjectNode() + .put("status", "succeeded") + .put("message", "account " + id + " created"); + } +} diff --git a/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Deposit.java b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Deposit.java new file mode 100644 index 00000000..1616a6ec --- /dev/null +++ b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Deposit.java @@ -0,0 +1,42 @@ +package com.scalar.application.bankaccount.contract; + +import com.fasterxml.jackson.databind.JsonNode; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; +import com.scalar.dl.ledger.exception.ContractContextException; +import com.scalar.dl.ledger.statemachine.Ledger; +import java.util.Optional; +import javax.annotation.Nullable; + +public class Deposit extends JacksonBasedContract { + @Override + public JsonNode invoke( + Ledger ledger, JsonNode argument, @Nullable JsonNode properties) { + if (!argument.has("id") || !argument.has("amount")) { + throw new ContractContextException("a required key is missing: id and/or amount"); + } + + String id = argument.get("id").asText(); + long amount = argument.get("amount").asLong(); + + if (amount < 0) { + throw new ContractContextException("amount is negative"); + } + + Optional> asset = ledger.get(id); + + if (!asset.isPresent()) { + throw new ContractContextException("account does not exist"); + } + + long oldBalance = asset.get().data().get("balance").asLong(); + long newBalance = oldBalance + amount; + + ledger.put(id, getObjectMapper().createObjectNode().put("balance", newBalance)); + return getObjectMapper() + .createObjectNode() + .put("status", "succeeded") + .put("old_balance", oldBalance) + .put("new_balance", newBalance); + } +} diff --git a/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Transfer.java b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Transfer.java new file mode 100644 index 00000000..856637c0 --- /dev/null +++ b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Transfer.java @@ -0,0 +1,57 @@ +package com.scalar.application.bankaccount.contract; + +import com.fasterxml.jackson.databind.JsonNode; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; +import com.scalar.dl.ledger.exception.ContractContextException; +import com.scalar.dl.ledger.statemachine.Ledger; +import java.util.Optional; +import javax.annotation.Nullable; + +public class Transfer extends JacksonBasedContract { + @Override + public JsonNode invoke(Ledger ledger, JsonNode argument, @Nullable JsonNode property) { + if (!(argument.has("from") && argument.has("to") && argument.has("amount"))) { + throw new ContractContextException("a required key is missing: from, to, and/or amount"); + } + + String fromId = argument.get("from").asText(); + String toId = argument.get("to").asText(); + long amount = argument.get("amount").asLong(); + + if (amount < 0) { + throw new ContractContextException("amount is negative"); + } + + Optional> fromAsset = ledger.get(fromId); + Optional> toAsset = ledger.get(toId); + + if (!fromAsset.isPresent()) { + throw new ContractContextException("from account does not exist"); + } + + if (!toAsset.isPresent()) { + throw new ContractContextException("to account does not exist"); + } + + long fromOldBalance = fromAsset.get().data().get("balance").asLong(); + long fromNewBalance = fromOldBalance - amount; + long toOldBalance = toAsset.get().data().get("balance").asLong(); + long toNewBalance = toOldBalance + amount; + + if (fromNewBalance < 0) { + throw new ContractContextException("insufficient funds"); + } + + ledger.put(fromId, getObjectMapper().createObjectNode().put("balance", fromNewBalance)); + ledger.put(toId, getObjectMapper().createObjectNode().put("balance", toNewBalance)); + + return getObjectMapper() + .createObjectNode() + .put("status", "succeeded") + .put("from_old_balance", fromOldBalance) + .put("from_new_balance", fromNewBalance) + .put("to_old_balance", toOldBalance) + .put("to_new_balance", toNewBalance); + } +} diff --git a/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Withdraw.java b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Withdraw.java new file mode 100644 index 00000000..42595af4 --- /dev/null +++ b/docs/applications/simple-bank-account/contract/src/main/java/com/scalar/application/bankaccount/contract/Withdraw.java @@ -0,0 +1,46 @@ +package com.scalar.application.bankaccount.contract; + +import com.fasterxml.jackson.databind.JsonNode; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; +import com.scalar.dl.ledger.exception.ContractContextException; +import com.scalar.dl.ledger.statemachine.Ledger; + +import javax.annotation.Nullable; +import java.util.Optional; + +public class Withdraw extends JacksonBasedContract { + @Override + public JsonNode invoke(Ledger ledger, JsonNode argument, @Nullable JsonNode property) { + if (!(argument.has("id") && argument.has("amount"))) { + throw new ContractContextException("a required key is missing: id and/or amount"); + } + + String id = argument.get("id").asText(); + long amount = argument.get("amount").asLong(); + + if (amount < 0) { + throw new ContractContextException("amount is negative"); + } + + Optional> response = ledger.get(id); + + if (!response.isPresent()) { + throw new ContractContextException("account does not exist"); + } + + long oldBalance = response.get().data().get("balance").asLong(); + long newBalance = oldBalance - amount; + + if (newBalance < 0) { + throw new ContractContextException("insufficient funds"); + } + + ledger.put(id, getObjectMapper().createObjectNode().put("balance", newBalance)); + return getObjectMapper() + .createObjectNode() + .put("status", "succeeded") + .put("old_balance", oldBalance) + .put("new_balance", newBalance); + } +} diff --git a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/AccountHistoryTest.java b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/AccountHistoryTest.java similarity index 59% rename from docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/AccountHistoryTest.java rename to docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/AccountHistoryTest.java index 3d258a53..2156235a 100644 --- a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/AccountHistoryTest.java +++ b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/AccountHistoryTest.java @@ -4,16 +4,16 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.verify; -import com.scalar.dl.ledger.contract.Contract; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.scalar.dl.ledger.database.AssetFilter; +import com.scalar.dl.ledger.contract.JacksonBasedContract; import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; -import java.util.Optional; +import com.scalar.dl.ledger.statemachine.Ledger; import java.util.UUID; -import javax.json.Json; -import javax.json.JsonObject; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -29,21 +29,27 @@ public class AccountHistoryTest { private final String ORDER_KEY = "order"; private final int START = 3; private final String START_KEY = "start"; - private final Contract contract = new AccountHistory(); - @Mock private Ledger ledger; + private final JacksonBasedContract contract = new AccountHistory(); + @Mock private Ledger ledger; + private AutoCloseable closeable; - @Before + @BeforeEach public void setUp() { - MockitoAnnotations.initMocks(this); + closeable = MockitoAnnotations.openMocks(this); + } + + @AfterEach + public void tearDown() throws Exception { + closeable.close(); } @Test public void invoke_EmptyArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().build(); + JsonNode argument = new ObjectMapper().createObjectNode(); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -52,10 +58,10 @@ public void invoke_EmptyArgument_ShouldThrowContractContextException() { public void invoke_ArgumentWithId_ShouldCallScanCorrectly() { // Arrange ArgumentCaptor filter = ArgumentCaptor.forClass(AssetFilter.class); - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID); // Act - contract.invoke(ledger, argument, Optional.empty()); + contract.invoke(ledger, argument, null); // Assert verify(ledger).scan(filter.capture()); @@ -66,15 +72,15 @@ public void invoke_ArgumentWithId_ShouldCallScanCorrectly() { public void invoke_ArgumentWithStartVersion_ShouldCallScanCorrectly() { // Arrange ArgumentCaptor filter = ArgumentCaptor.forClass(AssetFilter.class); - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(START_KEY, 3).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(START_KEY, 3); // Act - contract.invoke(ledger, argument, Optional.empty()); + contract.invoke(ledger, argument, null); // Assert verify(ledger).scan(filter.capture()); assertThat(filter.getValue().getId()).isEqualTo(ID); - assertThat(filter.getValue().getStartVersion().get()).isEqualTo(3); + assertThat(filter.getValue().getStartAge().get()).isEqualTo(3); assertThat(filter.getValue().isStartInclusive()).isEqualTo(true); } @@ -82,15 +88,15 @@ public void invoke_ArgumentWithStartVersion_ShouldCallScanCorrectly() { public void invoke_ArgumentWithEndVersion_ShouldCallScanCorrectly() { // Arrange ArgumentCaptor filter = ArgumentCaptor.forClass(AssetFilter.class); - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(END_KEY, END).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(END_KEY, END); // Act - contract.invoke(ledger, argument, Optional.empty()); + contract.invoke(ledger, argument, null); // Assert verify(ledger).scan(filter.capture()); assertThat(filter.getValue().getId()).isEqualTo(ID); - assertThat(filter.getValue().getEndVersion().get()).isEqualTo(END); + assertThat(filter.getValue().getEndAge().get()).isEqualTo(END); assertThat(filter.getValue().isEndInclusive()).isEqualTo(false); } @@ -98,10 +104,10 @@ public void invoke_ArgumentWithEndVersion_ShouldCallScanCorrectly() { public void invoke_ArgumentWithLimit_ShouldCallScanCorrectly() { // Arrange ArgumentCaptor filter = ArgumentCaptor.forClass(AssetFilter.class); - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(LIMIT_KEY, LIMIT).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(LIMIT_KEY, LIMIT); // Act - contract.invoke(ledger, argument, Optional.empty()); + contract.invoke(ledger, argument, null); // Assert verify(ledger).scan(filter.capture()); @@ -110,44 +116,44 @@ public void invoke_ArgumentWithLimit_ShouldCallScanCorrectly() { } @Test - public void invoke_ArgumentWithAscVersionOrderSpecified_ShouldCallScanCorrectly() { + public void invoke_ArgumentWithAscAgeOrderSpecified_ShouldCallScanCorrectly() { // Arrange ArgumentCaptor filter = ArgumentCaptor.forClass(AssetFilter.class); - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(ORDER_KEY, ASC).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(ORDER_KEY, ASC); // Act - contract.invoke(ledger, argument, Optional.empty()); + contract.invoke(ledger, argument, null); // Assert verify(ledger).scan(filter.capture()); assertThat(filter.getValue().getId()).isEqualTo(ID); - assertThat(filter.getValue().getVersionOrder().get()).isEqualTo(AssetFilter.VersionOrder.ASC); + assertThat(filter.getValue().getAgeOrder().get()).isEqualTo(AssetFilter.AgeOrder.ASC); } @Test public void invoke_ArgumentWithEverythingSpecified_ShouldCallScanCorrectly() { // Arrange ArgumentCaptor filter = ArgumentCaptor.forClass(AssetFilter.class); - JsonObject argument = - Json.createObjectBuilder() - .add(ID_KEY, ID) - .add(START_KEY, START) - .add(END_KEY, END) - .add(LIMIT_KEY, LIMIT) - .add(ORDER_KEY, ASC) - .build(); + JsonNode argument = + new ObjectMapper() + .createObjectNode() + .put(ID_KEY, ID) + .put(START_KEY, START) + .put(END_KEY, END) + .put(LIMIT_KEY, LIMIT) + .put(ORDER_KEY, ASC); // Act - contract.invoke(ledger, argument, Optional.empty()); + contract.invoke(ledger, argument, null); // Assert verify(ledger).scan(filter.capture()); assertThat(filter.getValue().getId()).isEqualTo(ID); - assertThat(filter.getValue().getStartVersion().get()).isEqualTo(START); + assertThat(filter.getValue().getStartAge().get()).isEqualTo(START); assertThat(filter.getValue().isStartInclusive()).isEqualTo(true); - assertThat(filter.getValue().getEndVersion().get()).isEqualTo(END); + assertThat(filter.getValue().getEndAge().get()).isEqualTo(END); assertThat(filter.getValue().isEndInclusive()).isEqualTo(false); assertThat(filter.getValue().getLimit()).isEqualTo(LIMIT); - assertThat(filter.getValue().getVersionOrder().get()).isEqualTo(AssetFilter.VersionOrder.ASC); + assertThat(filter.getValue().getAgeOrder().get()).isEqualTo(AssetFilter.AgeOrder.ASC); } } diff --git a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/CreateAccountTest.java b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/CreateAccountTest.java similarity index 50% rename from docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/CreateAccountTest.java rename to docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/CreateAccountTest.java index d15c6a24..f34be789 100644 --- a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/CreateAccountTest.java +++ b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/CreateAccountTest.java @@ -4,38 +4,46 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; +import com.scalar.dl.ledger.statemachine.Ledger; import java.util.Optional; import java.util.UUID; -import javax.json.Json; -import javax.json.JsonObject; -import org.junit.Before; -import org.junit.Test; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; public class CreateAccountTest { private final String ID = UUID.randomUUID().toString(); private final String ID_KEY = "id"; - private final Contract contract = new CreateAccount(); - @Mock private Ledger ledger; - @Mock private Asset asset; + private final JacksonBasedContract contract = new CreateAccount(); + @Mock private Ledger ledger; + @Mock private Asset asset; + private AutoCloseable closeable; - @Before + @BeforeEach public void setUp() { - MockitoAnnotations.initMocks(this); + closeable = MockitoAnnotations.openMocks(this); + } + + @AfterEach + public void tearDown() throws Exception { + closeable.close(); } @Test public void invoke_EmptyArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().build(); + JsonNode argument = new ObjectMapper().createObjectNode(); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -43,24 +51,24 @@ public void invoke_EmptyArgument_ShouldThrowContractContextException() { @Test public void invoke_AccountDoesNotExist_ShouldReturnSucceeded() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID); when(ledger.get(ID)).thenReturn(Optional.empty()); // Act - JsonObject response = contract.invoke(ledger, argument, Optional.empty()); + JsonNode response = contract.invoke(ledger, argument, null); // Assert - assertThat(response.getString("status")).isEqualTo("succeeded"); + assertThat(response.get("status").asText()).isEqualTo("succeeded"); } @Test public void invoke_AccountExists_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID); when(ledger.get(ID)).thenReturn(Optional.of(asset)); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("account already exists"); } diff --git a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/DepositTest.java b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/DepositTest.java similarity index 53% rename from docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/DepositTest.java rename to docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/DepositTest.java index 27eb2240..badf4136 100644 --- a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/DepositTest.java +++ b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/DepositTest.java @@ -4,16 +4,18 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; +import com.scalar.dl.ledger.statemachine.Ledger; import java.util.Optional; import java.util.UUID; -import javax.json.Json; -import javax.json.JsonObject; -import org.junit.Before; -import org.junit.Test; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -23,23 +25,29 @@ public class DepositTest { private final String ID = UUID.randomUUID().toString(); private final String ID_KEY = "id"; private final String STATUS_KEY = "status"; - private final Contract contract = new Deposit(); - @Mock private Ledger ledger; - @Mock private Asset asset; + private final JacksonBasedContract contract = new Deposit(); + @Mock private Ledger ledger; + @Mock private Asset asset; + private AutoCloseable closeable; - @Before + @BeforeEach public void setUp() { - MockitoAnnotations.initMocks(this); - when(asset.data()).thenReturn(Json.createObjectBuilder().add(BALANCE_KEY, 1).build()); + closeable = MockitoAnnotations.openMocks(this); + when(asset.data()).thenReturn(new ObjectMapper().createObjectNode().put(BALANCE_KEY, 1)); + } + + @AfterEach + public void tearDown() throws Exception { + closeable.close(); } @Test public void invoke_EmptyArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().build(); + JsonNode argument = new ObjectMapper().createObjectNode(); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -47,10 +55,10 @@ public void invoke_EmptyArgument_ShouldThrowContractContextException() { @Test public void invoke_IdKeyNotSuppliedInArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(AMOUNT_KEY, 1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(AMOUNT_KEY, 1); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -58,10 +66,10 @@ public void invoke_IdKeyNotSuppliedInArgument_ShouldThrowContractContextExceptio @Test public void invoke_AmountKeyNotSuppliedInArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, 1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, 1); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -69,26 +77,26 @@ public void invoke_AmountKeyNotSuppliedInArgument_ShouldThrowContractContextExce @Test public void invoke_AccountExists_ShouldIncreaseBalanceByAppropriateAmount() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(AMOUNT_KEY, 1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(AMOUNT_KEY, 1); when(ledger.get(ID)).thenReturn(Optional.of(asset)); // Act - JsonObject response = contract.invoke(ledger, argument, Optional.empty()); + JsonNode response = contract.invoke(ledger, argument, null); // Arrange - assertThat(response.getString(STATUS_KEY)).isEqualTo("succeeded"); - assertThat(response.getInt("old_balance")).isEqualTo(1); - assertThat(response.getInt("new_balance")).isEqualTo(2); + assertThat(response.get(STATUS_KEY).asText()).isEqualTo("succeeded"); + assertThat(response.get("old_balance").asInt()).isEqualTo(1); + assertThat(response.get("new_balance").asInt()).isEqualTo(2); } @Test public void invoke_AccountDoesNotExist_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(AMOUNT_KEY, 1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(AMOUNT_KEY, 1); when(ledger.get(ID)).thenReturn(Optional.empty()); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("account does not exist"); } @@ -96,11 +104,11 @@ public void invoke_AccountDoesNotExist_ShouldThrowContractContextException() { @Test public void invoke_DepositAmountIsNegative_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(AMOUNT_KEY, -1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(AMOUNT_KEY, -1); when(ledger.get(ID)).thenReturn(Optional.of(asset)); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("amount is negative"); } diff --git a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/TransferTest.java b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/TransferTest.java similarity index 52% rename from docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/TransferTest.java rename to docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/TransferTest.java index fcbe2355..0ebbca1c 100644 --- a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/TransferTest.java +++ b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/TransferTest.java @@ -4,16 +4,17 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; +import com.scalar.dl.ledger.statemachine.Ledger; import java.util.Optional; import java.util.UUID; -import javax.json.Json; -import javax.json.JsonObject; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -25,25 +26,31 @@ public class TransferTest { private final String STATUS_KEY = "status"; private final String TO = UUID.randomUUID().toString(); private final String TO_KEY = "to"; - private final Contract contract = new Transfer(); - @Mock private Ledger ledger; - @Mock private Asset fromAsset; - @Mock private Asset toAsset; + private final JacksonBasedContract contract = new Transfer(); + @Mock private Ledger ledger; + @Mock private Asset fromAsset; + @Mock private Asset toAsset; + private AutoCloseable closeable; - @Before + @BeforeEach public void setUp() { - MockitoAnnotations.initMocks(this); - when(fromAsset.data()).thenReturn(Json.createObjectBuilder().add(BALANCE_KEY, 1).build()); - when(toAsset.data()).thenReturn(Json.createObjectBuilder().add(BALANCE_KEY, 0).build()); + closeable = MockitoAnnotations.openMocks(this); + when(fromAsset.data()).thenReturn(new ObjectMapper().createObjectNode().put(BALANCE_KEY, 1)); + when(toAsset.data()).thenReturn(new ObjectMapper().createObjectNode().put(BALANCE_KEY, 0)); + } + + @AfterEach + public void tearDown() throws Exception { + closeable.close(); } @Test public void invoke_EmptyArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().build(); + JsonNode argument = new ObjectMapper().createObjectNode(); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -51,10 +58,10 @@ public void invoke_EmptyArgument_ShouldThrowContractContextException() { @Test public void invoke_FromKeyNotSuppliedInArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(TO_KEY, TO).add(AMOUNT_KEY, 1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(TO_KEY, TO).put(AMOUNT_KEY, 1); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -62,10 +69,11 @@ public void invoke_FromKeyNotSuppliedInArgument_ShouldThrowContractContextExcept @Test public void invoke_ToKeyNotSuppliedInArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(FROM_KEY, FROM).add(AMOUNT_KEY, 1).build(); + JsonNode argument = + new ObjectMapper().createObjectNode().put(FROM_KEY, FROM).put(AMOUNT_KEY, 1); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -73,10 +81,10 @@ public void invoke_ToKeyNotSuppliedInArgument_ShouldThrowContractContextExceptio @Test public void invoke_AmountKeyNotSuppliedInArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(TO_KEY, TO).add(FROM_KEY, FROM).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(TO_KEY, TO).put(FROM_KEY, FROM); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -84,32 +92,40 @@ public void invoke_AmountKeyNotSuppliedInArgument_ShouldThrowContractContextExce @Test public void invoke_AccountExists_ShouldTransferFundsByAppropriateAmount() { // Arrange - JsonObject argument = - Json.createObjectBuilder().add(FROM_KEY, FROM).add(TO_KEY, TO).add(AMOUNT_KEY, 1).build(); + JsonNode argument = + new ObjectMapper() + .createObjectNode() + .put(FROM_KEY, FROM) + .put(TO_KEY, TO) + .put(AMOUNT_KEY, 1); when(ledger.get(FROM)).thenReturn(Optional.of(fromAsset)); when(ledger.get(TO)).thenReturn(Optional.of(toAsset)); // Act - JsonObject response = contract.invoke(ledger, argument, Optional.empty()); + JsonNode response = contract.invoke(ledger, argument, null); // Arrange - assertThat(response.getString(STATUS_KEY)).isEqualTo("succeeded"); - assertThat(response.getInt("from_old_balance")).isEqualTo(1); - assertThat(response.getInt("from_new_balance")).isEqualTo(0); - assertThat(response.getInt("to_old_balance")).isEqualTo(0); - assertThat(response.getInt("to_new_balance")).isEqualTo(1); + assertThat(response.get(STATUS_KEY).asText()).isEqualTo("succeeded"); + assertThat(response.get("from_old_balance").asInt()).isEqualTo(1); + assertThat(response.get("from_new_balance").asInt()).isEqualTo(0); + assertThat(response.get("to_old_balance").asInt()).isEqualTo(0); + assertThat(response.get("to_new_balance").asInt()).isEqualTo(1); } @Test public void invoke_FromAccountDoesNotExist_ShouldThrowContractContextException() { // Arrange - JsonObject argument = - Json.createObjectBuilder().add(FROM_KEY, FROM).add(TO_KEY, TO).add(AMOUNT_KEY, 1).build(); + JsonNode argument = + new ObjectMapper() + .createObjectNode() + .put(FROM_KEY, FROM) + .put(TO_KEY, TO) + .put(AMOUNT_KEY, 1); when(ledger.get(FROM)).thenReturn(Optional.empty()); when(ledger.get(TO)).thenReturn(Optional.of(toAsset)); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("from account does not exist"); } @@ -117,13 +133,17 @@ public void invoke_FromAccountDoesNotExist_ShouldThrowContractContextException() @Test public void invoke_ToAccountDoesNotExist_ShouldThrowContractContextException() { // Arrange - JsonObject argument = - Json.createObjectBuilder().add(FROM_KEY, FROM).add(TO_KEY, TO).add(AMOUNT_KEY, 1).build(); + JsonNode argument = + new ObjectMapper() + .createObjectNode() + .put(FROM_KEY, FROM) + .put(TO_KEY, TO) + .put(AMOUNT_KEY, 1); when(ledger.get(FROM)).thenReturn(Optional.of(fromAsset)); when(ledger.get(TO)).thenReturn(Optional.empty()); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("to account does not exist"); } @@ -132,13 +152,17 @@ public void invoke_ToAccountDoesNotExist_ShouldThrowContractContextException() { public void invoke_DepositAmountIsNegative_ShouldThrowContractContextException() { // Arrange // Arrange - JsonObject argument = - Json.createObjectBuilder().add(FROM_KEY, FROM).add(TO_KEY, TO).add(AMOUNT_KEY, -1).build(); + JsonNode argument = + new ObjectMapper() + .createObjectNode() + .put(FROM_KEY, FROM) + .put(TO_KEY, TO) + .put(AMOUNT_KEY, -1); when(ledger.get(FROM)).thenReturn(Optional.empty()); when(ledger.get(TO)).thenReturn(Optional.of(toAsset)); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("amount is negative"); } @@ -146,13 +170,17 @@ public void invoke_DepositAmountIsNegative_ShouldThrowContractContextException() @Test public void invoke_TransferMoreThanCurrentBalance_ShouldThrowContractContextException() { // Arrange - JsonObject argument = - Json.createObjectBuilder().add(FROM_KEY, FROM).add(TO_KEY, TO).add(AMOUNT_KEY, 2).build(); + JsonNode argument = + new ObjectMapper() + .createObjectNode() + .put(FROM_KEY, FROM) + .put(TO_KEY, TO) + .put(AMOUNT_KEY, 2); when(ledger.get(FROM)).thenReturn(Optional.of(fromAsset)); when(ledger.get(TO)).thenReturn(Optional.of(toAsset)); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("insufficient funds"); } diff --git a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/WithdrawTest.java b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/WithdrawTest.java similarity index 54% rename from docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/WithdrawTest.java rename to docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/WithdrawTest.java index 0523361c..606e7e98 100644 --- a/docs/applications/simple-bank-account/src/test/java/com/scalar/application/bankaccount/contract/WithdrawTest.java +++ b/docs/applications/simple-bank-account/contract/src/test/java/com/scalar/application/bankaccount/contract/WithdrawTest.java @@ -4,16 +4,17 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scalar.dl.ledger.statemachine.Asset; +import com.scalar.dl.ledger.contract.JacksonBasedContract; import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; +import com.scalar.dl.ledger.statemachine.Ledger; import java.util.Optional; import java.util.UUID; -import javax.json.Json; -import javax.json.JsonObject; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -23,23 +24,29 @@ public class WithdrawTest { private final String ID = UUID.randomUUID().toString(); private final String ID_KEY = "id"; private final String STATUS_KEY = "status"; - private final Contract contract = new Withdraw(); - @Mock private Ledger ledger; - @Mock private Asset asset; + private final JacksonBasedContract contract = new Withdraw(); + @Mock private Ledger ledger; + @Mock private Asset asset; + private AutoCloseable closeable; - @Before + @BeforeEach public void setUp() { - MockitoAnnotations.initMocks(this); - when(asset.data()).thenReturn(Json.createObjectBuilder().add(BALANCE_KEY, 1).build()); + closeable = MockitoAnnotations.openMocks(this); + when(asset.data()).thenReturn(new ObjectMapper().createObjectNode().put(BALANCE_KEY, 1)); + } + + @AfterEach + public void tearDown() throws Exception { + closeable.close(); } @Test public void invoke_EmptyArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().build(); + JsonNode argument = new ObjectMapper().createObjectNode(); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -47,10 +54,10 @@ public void invoke_EmptyArgument_ShouldThrowContractContextException() { @Test public void invoke_IdKeyNotSuppliedInArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(AMOUNT_KEY, 1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(AMOUNT_KEY, 1); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -58,10 +65,10 @@ public void invoke_IdKeyNotSuppliedInArgument_ShouldThrowContractContextExceptio @Test public void invoke_AmountKeyNotSuppliedInArgument_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, 1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, 1); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessageStartingWith("a required key is missing:"); } @@ -69,26 +76,26 @@ public void invoke_AmountKeyNotSuppliedInArgument_ShouldThrowContractContextExce @Test public void invoke_AccountExists_ShouldDecreaseBalanceByAppropriateAmount() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(AMOUNT_KEY, 1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(AMOUNT_KEY, 1); when(ledger.get(ID)).thenReturn(Optional.of(asset)); // Act - JsonObject response = contract.invoke(ledger, argument, Optional.empty()); + JsonNode response = contract.invoke(ledger, argument, null); // Arrange - assertThat(response.getString(STATUS_KEY)).isEqualTo("succeeded"); - assertThat(response.getInt("old_balance")).isEqualTo(1); - assertThat(response.getInt("new_balance")).isEqualTo(0); + assertThat(response.get(STATUS_KEY).asText()).isEqualTo("succeeded"); + assertThat(response.get("old_balance").asInt()).isEqualTo(1); + assertThat(response.get("new_balance").asInt()).isEqualTo(0); } @Test public void invoke_AccountDoesNotExist_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(AMOUNT_KEY, 1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(AMOUNT_KEY, 1); when(ledger.get(ID)).thenReturn(Optional.empty()); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("account does not exist"); } @@ -96,11 +103,11 @@ public void invoke_AccountDoesNotExist_ShouldThrowContractContextException() { @Test public void invoke_DepositAmountIsNegative_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(AMOUNT_KEY, -1).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(AMOUNT_KEY, -1); when(ledger.get(ID)).thenReturn(Optional.of(asset)); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("amount is negative"); } @@ -108,11 +115,11 @@ public void invoke_DepositAmountIsNegative_ShouldThrowContractContextException() @Test public void invoke_WithdrawMoreThanCurrentBalance_ShouldThrowContractContextException() { // Arrange - JsonObject argument = Json.createObjectBuilder().add(ID_KEY, ID).add(AMOUNT_KEY, 2).build(); + JsonNode argument = new ObjectMapper().createObjectNode().put(ID_KEY, ID).put(AMOUNT_KEY, 2); when(ledger.get(ID)).thenReturn(Optional.of(asset)); // Act-assert - assertThatThrownBy(() -> contract.invoke(ledger, argument, Optional.empty())) + assertThatThrownBy(() -> contract.invoke(ledger, argument, null)) .isInstanceOf(ContractContextException.class) .hasMessage("insufficient funds"); } diff --git a/docs/applications/simple-bank-account/gradle/wrapper/gradle-wrapper.jar b/docs/applications/simple-bank-account/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/docs/applications/simple-bank-account/gradlew.bat b/docs/applications/simple-bank-account/gradlew.bat index e95643d6..7101f8e4 100644 --- a/docs/applications/simple-bank-account/gradlew.bat +++ b/docs/applications/simple-bank-account/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,25 +25,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,48 +55,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/docs/applications/simple-bank-account/register b/docs/applications/simple-bank-account/register deleted file mode 100755 index d9d68c11..00000000 --- a/docs/applications/simple-bank-account/register +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -${SCALAR_SDK_HOME}/client/bin/scalardl register-cert -properties ./conf/client.properties -${SCALAR_SDK_HOME}/client/bin/scalardl register-contracts -properties ./conf/client.properties -contracts-toml-file ./conf/contracts.toml diff --git a/docs/applications/simple-bank-account/settings.gradle b/docs/applications/simple-bank-account/settings.gradle index 0ff74490..5486562b 100644 --- a/docs/applications/simple-bank-account/settings.gradle +++ b/docs/applications/simple-bank-account/settings.gradle @@ -1 +1,3 @@ rootProject.name = 'simple-bank-account' +include 'contract' +include 'app' \ No newline at end of file diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/AccountHistory.java b/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/AccountHistory.java deleted file mode 100644 index 61dc6a4d..00000000 --- a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/AccountHistory.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.scalar.application.bankaccount.contract; - -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; -import com.scalar.dl.ledger.database.AssetFilter; -import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; -import java.util.List; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObject; - -public class AccountHistory extends Contract { - @Override - public JsonObject invoke(Ledger ledger, JsonObject argument, Optional property) { - if (!argument.containsKey("id")) { - throw new ContractContextException("a required key is missing: id"); - } - - AssetFilter filter = new AssetFilter(argument.getString("id")); - if (argument.containsKey("start")) { - filter.withStartVersion(argument.getInt("start"), true); - } - if (argument.containsKey("end")) { - filter.withEndVersion(argument.getInt("end"), false); - } - if (argument.containsKey("limit")) { - filter.withLimit(argument.getInt("limit")); - } - if (argument.containsKey("order") && argument.getString("order").equals("asc")) { - filter.withVersionOrder(AssetFilter.VersionOrder.ASC); - } - - List history = ledger.scan(filter); - - JsonArrayBuilder result = Json.createArrayBuilder(); - history.forEach( - asset -> { - JsonObject json = - Json.createObjectBuilder() - .add("id", asset.id()) - .add("data", asset.data()) - .add("age", asset.age()) - .build(); - result.add(json); - }); - - return Json.createObjectBuilder() - .add("status", "succeeded") - .add("history", result.build()) - .build(); - } -} diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/CreateAccount.java b/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/CreateAccount.java deleted file mode 100644 index 6d9b90b6..00000000 --- a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/CreateAccount.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.scalar.application.bankaccount.contract; - -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; -import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonObject; - -public class CreateAccount extends Contract { - @Override - public JsonObject invoke(Ledger ledger, JsonObject argument, Optional property) { - if (!argument.containsKey("id")) { - throw new ContractContextException("a required key is missing: id"); - } - - String id = argument.getString("id"); - Optional response = ledger.get(id); - - if (response.isPresent()) { - throw new ContractContextException("account already exists"); - } - - ledger.put(id, Json.createObjectBuilder().add("balance", 0).build()); - return Json.createObjectBuilder() - .add("status", "succeeded") - .add("message", "account " + id + " created") - .build(); - } -} diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Deposit.java b/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Deposit.java deleted file mode 100644 index 088995a6..00000000 --- a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Deposit.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.scalar.application.bankaccount.contract; - -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; -import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonObject; - -public class Deposit extends Contract { - @Override - public JsonObject invoke(Ledger ledger, JsonObject argument, Optional property) { - if (!(argument.containsKey("id") && argument.containsKey("amount"))) { - throw new ContractContextException("a required key is missing: id and/or amount"); - } - - String id = argument.getString("id"); - long amount = argument.getJsonNumber("amount").longValue(); - - if (amount < 0) { - throw new ContractContextException("amount is negative"); - } - - Optional response = ledger.get(id); - - if (!response.isPresent()) { - throw new ContractContextException("account does not exist"); - } - - long oldBalance = response.get().data().getInt("balance"); - long newBalance = oldBalance + amount; - - ledger.put(id, Json.createObjectBuilder().add("balance", newBalance).build()); - return Json.createObjectBuilder() - .add("status", "succeeded") - .add("old_balance", oldBalance) - .add("new_balance", newBalance) - .build(); - } -} diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Transfer.java b/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Transfer.java deleted file mode 100644 index cba5fa0a..00000000 --- a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Transfer.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.scalar.application.bankaccount.contract; - -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; -import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonObject; - -public class Transfer extends Contract { - @Override - public JsonObject invoke(Ledger ledger, JsonObject argument, Optional property) { - if (!(argument.containsKey("from") - && argument.containsKey("to") - && argument.containsKey("amount"))) { - throw new ContractContextException("a required key is missing: from, to, and/or amount"); - } - - String fromId = argument.getString("from"); - String toId = argument.getString("to"); - long amount = argument.getJsonNumber("amount").longValue(); - - if (amount < 0) { - throw new ContractContextException("amount is negative"); - } - - Optional fromAsset = ledger.get(fromId); - Optional toAsset = ledger.get(toId); - - if (!fromAsset.isPresent()) { - throw new ContractContextException("from account does not exist"); - } - - if (!toAsset.isPresent()) { - throw new ContractContextException("to account does not exist"); - } - - long fromOldBalance = fromAsset.get().data().getInt("balance"); - long fromNewBalance = fromOldBalance - amount; - long toOldBalance = toAsset.get().data().getInt("balance"); - long toNewBalance = toOldBalance + amount; - - if (fromNewBalance < 0) { - throw new ContractContextException("insufficient funds"); - } - - ledger.put(fromId, Json.createObjectBuilder().add("balance", fromNewBalance).build()); - ledger.put(toId, Json.createObjectBuilder().add("balance", toNewBalance).build()); - return Json.createObjectBuilder() - .add("status", "succeeded") - .add("from_old_balance", fromOldBalance) - .add("from_new_balance", fromNewBalance) - .add("to_old_balance", toOldBalance) - .add("to_new_balance", toNewBalance) - .build(); - } -} diff --git a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Withdraw.java b/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Withdraw.java deleted file mode 100644 index aba9ccf0..00000000 --- a/docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/contract/Withdraw.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.scalar.application.bankaccount.contract; - -import com.scalar.dl.ledger.asset.Asset; -import com.scalar.dl.ledger.contract.Contract; -import com.scalar.dl.ledger.exception.ContractContextException; -import com.scalar.dl.ledger.database.Ledger; -import java.util.Optional; -import javax.json.Json; -import javax.json.JsonObject; - -public class Withdraw extends Contract { - @Override - public JsonObject invoke(Ledger ledger, JsonObject argument, Optional property) { - if (!(argument.containsKey("id") && argument.containsKey("amount"))) { - throw new ContractContextException("a required key is missing: id and/or amount"); - } - - String id = argument.getString("id"); - long amount = argument.getJsonNumber("amount").longValue(); - - if (amount < 0) { - throw new ContractContextException("amount is negative"); - } - - Optional response = ledger.get(id); - - if (!response.isPresent()) { - throw new ContractContextException("account does not exist"); - } - - long oldBalance = response.get().data().getInt("balance"); - long newBalance = oldBalance - amount; - - if (newBalance < 0) { - throw new ContractContextException("insufficient funds"); - } - - ledger.put(id, Json.createObjectBuilder().add("balance", newBalance).build()); - return Json.createObjectBuilder() - .add("status", "succeeded") - .add("old_balance", oldBalance) - .add("new_balance", newBalance) - .build(); - } -}