Skip to content

Commit

Permalink
Update simple bank account app (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
KodaiD authored Aug 15, 2024
1 parent 4a817b7 commit 33b402c
Show file tree
Hide file tree
Showing 37 changed files with 818 additions and 635 deletions.
76 changes: 39 additions & 37 deletions docs/applications/simple-bank-account/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)

Expand All @@ -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.
Expand All @@ -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<JsonObject> property) {
if (!(argument.containsKey("id") && argument.containsKey("amount"))) {
public JsonNode invoke(
Ledger<JsonNode> 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<Asset> response = ledger.get(id);
Optional<Asset<JsonNode>> 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`.
Expand All @@ -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

Expand All @@ -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.
Expand All @@ -149,15 +154,15 @@ 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.

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.
Expand All @@ -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).
Expand All @@ -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:
Expand Down
53 changes: 53 additions & 0 deletions docs/applications/simple-bank-account/app/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
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;
import org.springframework.beans.factory.annotation.Value;
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";
Expand All @@ -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) {
Expand All @@ -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;
}
Expand All @@ -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() : "{}")
+ ")");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private ResponseEntity<String> 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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
client.properties.path=conf/client.properties
client.properties.path=../conf/client.properties

# Contract ids
contract.id.account-history=account-history
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down
40 changes: 0 additions & 40 deletions docs/applications/simple-bank-account/build.gradle

This file was deleted.

4 changes: 2 additions & 2 deletions docs/applications/simple-bank-account/conf/client.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 33b402c

Please sign in to comment.