Skip to content

Commit 33b402c

Browse files
authored
Update simple bank account app (#61)
1 parent 4a817b7 commit 33b402c

File tree

37 files changed

+818
-635
lines changed

37 files changed

+818
-635
lines changed

docs/applications/simple-bank-account/README.md

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ This application uses five contracts:
2020
- `Transfer.java`
2121
- `Withdraw.java`
2222

23-
(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.
23+
(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.
2424

2525
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.)
2626

@@ -31,6 +31,8 @@ The overall architecture of this application can be viewed as follows. (Note aga
3131
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
3232

3333
```
34+
$ ./gradlew build
35+
$ cd contract
3436
$ SCALAR_SDK_HOME=/path/to/scalardl-client-sdk ./register
3537
```
3638
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
4345

4446
### Contracts
4547

46-
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.
48+
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.
4749

4850
```java
4951
package com.scalar.application.bankaccount.contract;
5052

51-
import com.scalar.dl.ledger.asset.Asset;
52-
import com.scalar.dl.ledger.contract.Contract;
53+
import com.fasterxml.jackson.databind.JsonNode;
54+
import com.scalar.dl.ledger.statemachine.Asset;
55+
import com.scalar.dl.ledger.contract.JacksonBasedContract;
5356
import com.scalar.dl.ledger.exception.ContractContextException;
54-
import com.scalar.dl.ledger.database.Ledger;
57+
import com.scalar.dl.ledger.statemachine.Ledger;
5558
import java.util.Optional;
56-
import javax.json.Json;
57-
import javax.json.JsonObject;
59+
import javax.annotation.Nullable;
5860

59-
public class Deposit extends Contract {
61+
public class Deposit extends JacksonBasedContract {
6062
@Override
61-
public JsonObject invoke(Ledger ledger, JsonObject argument, Optional<JsonObject> property) {
62-
if (!(argument.containsKey("id") && argument.containsKey("amount"))) {
63+
public JsonNode invoke(
64+
Ledger<JsonNode> ledger, JsonNode argument, @Nullable JsonNode properties) {
65+
if (!argument.has("id") || !argument.has("amount")) {
6366
throw new ContractContextException("a required key is missing: id and/or amount");
6467
}
6568

66-
String id = argument.getString("id");
67-
long amount = argument.getJsonNumber("amount").longValue();
69+
String id = argument.get("id").asText();
70+
long amount = argument.get("amount").asLong();
6871

6972
if (amount < 0) {
7073
throw new ContractContextException("amount is negative");
7174
}
7275

73-
Optional<Asset> response = ledger.get(id);
76+
Optional<Asset<JsonNode>> asset = ledger.get(id);
7477

75-
if (!response.isPresent()) {
78+
if (!asset.isPresent()) {
7679
throw new ContractContextException("account does not exist");
7780
}
7881

79-
long oldBalance = response.get().data().getInt("balance");
82+
long oldBalance = asset.get().data().get("balance").asLong();
8083
long newBalance = oldBalance + amount;
8184

82-
ledger.put(id, Json.createObjectBuilder().add("balance", newBalance).build());
83-
return Json.createObjectBuilder()
84-
.add("status", "succeeded")
85-
.add("old_balance", oldBalance)
86-
.add("new_balance", newBalance)
87-
.build();
85+
ledger.put(id, getObjectMapper().createObjectNode().put("balance", newBalance));
86+
return getObjectMapper()
87+
.createObjectNode()
88+
.put("status", "succeeded")
89+
.put("old_balance", oldBalance)
90+
.put("new_balance", newBalance);
8891
}
8992
}
93+
9094
```
9195

9296
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
9599

96100
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`.
97101

98-
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`.
102+
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`.
99103

100104
**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`.
101105

102106
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.
103107

104-
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`.
108+
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`.
105109

106-
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).
110+
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).
107111

108112
Once you have written your contracts you will need to compile them, and this can be done as
109113

@@ -128,7 +132,8 @@ scalar.dl.client.private_key_path=conf/client-key.pem
128132
If everything is set up properly you should be able to register your certificate on the ScalarDL network as
129133

130134
```bash
131-
$ ${SCALAR_SDK_HOME}/client/bin/scalardl register-cert --properties ./conf/client.properties
135+
$ cd contract
136+
$ ${SCALAR_SDK_HOME}/client/bin/scalardl register-cert --properties ../conf/client.properties
132137
```
133138

134139
You should receive status code 200 if successful.
@@ -149,15 +154,15 @@ contract-class-file = "build/classes/java/main/com/scalar/application/bankaccoun
149154
[[contracts]]
150155
contract-id = "transfer"
151156
contract-binary-name = "com.scalar.application.bankaccount.contract.Transfer"
152-
contract-class-file = "build/classes/java/main/com/scalar/application/bankaccount/contract/Transfer.class"
157+
contract-class-file = "build/classes/java/main/com/scalar/application/bankaccount/contract/Transfer.class"
153158
```
154159

155160
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.
156161

157162
Once your toml file is written you can register all the specified contracts as
158163

159164
```bash
160-
$ ${SCALAR_SDK_HOME}/client/bin/scalardl register-contracts --properties ./conf/client.properties --contracts-file ./conf/contracts.toml
165+
$ ${SCALAR_SDK_HOME}/client/bin/scalardl register-contracts --properties ../conf/client.properties --contracts-file ../conf/contracts.toml
161166
```
162167

163168
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
169174
Create two accounts with ids `a111` and `b222`. (Contract ids can be any string.)
170175

171176
```bash
172-
$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ./conf/client.properties --contract-id create-account --contract-argument '{"id": "a111"}'
173-
$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ./conf/client.properties --contract-id create-account --contract-argument '{"id": "b222"}'
177+
$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ../conf/client.properties --contract-id create-account --contract-argument '{"id": "a111"}'
178+
$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ../conf/client.properties --contract-id create-account --contract-argument '{"id": "b222"}'
174179
```
175180

176181
Now, deposit 100 into account `a111`:
177182

178183
```bash
179-
$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ./conf/client.properties --contract-id deposit --contract-argument '{"id": "a111", "amount": 100}'
184+
$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ../conf/client.properties --contract-id deposit --contract-argument '{"id": "a111", "amount": 100}'
180185
```
181186

182187
Finally, transfer 25 from `a111` to `b222`:
183188

184189
```bash
185-
$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ./conf/client.properties --contract-id transfer --contract-argument '{"from": "a111", "to": "b222", "amount": 100}'
190+
$ ${SCALAR_SDK_HOME}/client/bin/scalardl execute-contract --properties ../conf/client.properties --contract-id transfer --contract-argument '{"from": "a111", "to": "b222", "amount": 100}'
186191
```
187192

188193
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
195200

196201
```groovy
197202
dependencies {
198-
compile group: 'com.scalar-labs', name: 'scalardl-java-client-sdk', version: '2.0.4'
203+
compile group: 'com.scalar-labs', name: 'scalardl-java-client-sdk', version: '3.9.1'
199204
}
200205
```
201206

202207
The following snippet shows how you can instantiate a `ClientService` object, where `properties` should be the path to your `client.properties` file.
203208

204209
```java
205-
Injector injector =
206-
Guice.createInjector(new ClientModule(new ClientConfig(new File(properties))));
207-
try (ClientService clientService = injector.getInstance(ClientService.class)) {
208-
...
209-
}
210+
ClientServiceFactory factory = new ClientServiceFactory();
211+
ClientService service = factory.create(new ClientConfig(new File(properties));
210212
```
211213

212214
`ClientService` contains a method `executeContract(String id, JsonObject argument)` which can be used to, of course, execute a contract. For example:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
buildscript {
2+
repositories {
3+
mavenCentral()
4+
}
5+
dependencies {
6+
classpath("org.springframework.boot:spring-boot-gradle-plugin:3.3.0")
7+
}
8+
}
9+
10+
plugins {
11+
id 'java'
12+
id 'application'
13+
id 'idea'
14+
id "org.springframework.boot" version "3.3.0"
15+
id "io.spring.dependency-management" version "1.1.5"
16+
}
17+
18+
application {
19+
mainClass = 'com.scalar.application.bankaccount.Application'
20+
}
21+
22+
bootJar {
23+
archiveBaseName = 'gs-rest-service'
24+
archiveVersion = '0.1.0'
25+
}
26+
27+
repositories {
28+
mavenCentral()
29+
}
30+
31+
java {
32+
toolchain {
33+
languageVersion = JavaLanguageVersion.of(21)
34+
}
35+
}
36+
37+
tasks.named('test') {
38+
useJUnitPlatform()
39+
}
40+
41+
group = 'com.scalar.application.simple-bank-account'
42+
version = '0.1'
43+
44+
dependencies {
45+
implementation('org.springframework.boot:spring-boot-starter-web') {
46+
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
47+
}
48+
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
49+
implementation group: 'com.scalar-labs', name: 'scalardl-java-client-sdk', version: '3.9.1'
50+
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
51+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
52+
testImplementation 'org.assertj:assertj-core:3.26.0'
53+
}

docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/Application.java renamed to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/Application.java

File renamed without changes.

docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/controller/AccountController.java renamed to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/controller/AccountController.java

File renamed without changes.

docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/controller/TransferController.java renamed to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/controller/TransferController.java

File renamed without changes.

docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/exception/ResourceNotFoundException.java renamed to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/exception/ResourceNotFoundException.java

File renamed without changes.

docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/model/Account.java renamed to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/model/Account.java

File renamed without changes.

docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/repository/AccountRepository.java renamed to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/repository/AccountRepository.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
package com.scalar.application.bankaccount.repository;
22

3-
import com.google.inject.Guice;
4-
import com.google.inject.Injector;
53
import com.scalar.dl.client.config.ClientConfig;
6-
import com.scalar.dl.client.service.ClientModule;
74
import com.scalar.dl.client.service.ClientService;
5+
import com.scalar.dl.client.service.ClientServiceFactory;
86
import com.scalar.dl.ledger.model.ContractExecutionResult;
97
import java.io.File;
108
import java.io.IOException;
11-
import javax.inject.Singleton;
129
import javax.json.JsonObject;
1310
import org.apache.logging.log4j.LogManager;
1411
import org.apache.logging.log4j.Logger;
1512
import org.springframework.beans.factory.annotation.Value;
1613
import org.springframework.stereotype.Repository;
1714

1815
@Repository
19-
@Singleton
2016
public class AccountRepository {
2117
private static final Logger logger = LogManager.getLogger(AccountRepository.class);
2218
private static final String ACCOUNT_HISTORY_ID = "account-history";
@@ -28,9 +24,8 @@ public class AccountRepository {
2824

2925
public AccountRepository(@Value("${client.properties.path}") String properties)
3026
throws IOException {
31-
Injector injector =
32-
Guice.createInjector(new ClientModule(new ClientConfig(new File(properties))));
33-
this.clientService = injector.getInstance(ClientService.class);
27+
ClientServiceFactory clientServiceFactory = new ClientServiceFactory();
28+
this.clientService = clientServiceFactory.create(new ClientConfig(new File(properties)));
3429
}
3530

3631
public ContractExecutionResult create(JsonObject argument) {
@@ -40,8 +35,7 @@ public ContractExecutionResult create(JsonObject argument) {
4035
}
4136

4237
public ContractExecutionResult history(JsonObject argument) {
43-
ContractExecutionResult result =
44-
clientService.executeContract(ACCOUNT_HISTORY_ID, argument);
38+
ContractExecutionResult result = clientService.executeContract(ACCOUNT_HISTORY_ID, argument);
4539
logResponse("history", result);
4640
return result;
4741
}
@@ -68,7 +62,7 @@ private void logResponse(String header, ContractExecutionResult result) {
6862
logger.info(
6963
header
7064
+ ": ("
71-
+ (result.getResult().isPresent() ? result.getResult().get() : "{}")
65+
+ (result.getContractResult().isPresent() ? result.getContractResult().get() : "{}")
7266
+ ")");
7367
}
7468
}

docs/applications/simple-bank-account/src/main/java/com/scalar/application/bankaccount/service/AccountService.java renamed to docs/applications/simple-bank-account/app/src/main/java/com/scalar/application/bankaccount/service/AccountService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ private ResponseEntity<String> serve(ThrowableFunction f, JsonObject json) {
7171
ContractExecutionResult result = f.apply(json);
7272

7373
return ResponseEntity
74-
.ok(result.getResult().isPresent() ? result.getResult().get().toString() : "{}");
74+
.ok(result.getContractResult().isPresent() ? result.getContractResult().get() : "{}");
7575
} catch (ClientException e) {
7676
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
7777
.body(Json.createObjectBuilder()

docs/applications/simple-bank-account/src/main/resources/application.properties renamed to docs/applications/simple-bank-account/app/src/main/resources/application.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
client.properties.path=conf/client.properties
1+
client.properties.path=../conf/client.properties
22

33
# Contract ids
44
contract.id.account-history=account-history

0 commit comments

Comments
 (0)