├── .circleci
└── config.yml
├── .gitignore
├── contract-service
├── .gitignore
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── pl
│ │ │ └── piomin
│ │ │ └── services
│ │ │ └── contract
│ │ │ ├── ContractApp.java
│ │ │ ├── controller
│ │ │ └── ContractController.java
│ │ │ ├── model
│ │ │ ├── Contract.java
│ │ │ └── Transaction.java
│ │ │ └── service
│ │ │ └── ContractService.java
│ └── resources
│ │ ├── application.yml
│ │ └── fee.sol
│ └── test
│ ├── java
│ └── pl
│ │ └── piomin
│ │ └── services
│ │ └── contract
│ │ └── ContractTest.java
│ └── resources
│ ├── jason_sullivan.json
│ └── john_smith.json
├── pom.xml
├── readme.md
├── renovate.json
└── transaction-service
├── .gitignore
├── pom.xml
└── src
└── main
├── java
└── pl
│ └── piomin
│ └── services
│ └── transaction
│ ├── TransactionApp.java
│ ├── controller
│ └── TransactionController.java
│ └── model
│ └── TransactionRequest.java
└── resources
└── application.yml
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: '2.1'
2 |
3 | jobs:
4 | analyze:
5 | docker:
6 | - image: 'cimg/openjdk:21.0.2'
7 | steps:
8 | - checkout
9 | - run:
10 | name: Analyze on SonarCloud
11 | command: mvn web3j:generate-sources verify sonar:sonar -DskipTests
12 | test:
13 | executor: machine_executor_amd64
14 | steps:
15 | - checkout
16 | - run:
17 | name: Install OpenJDK 21
18 | command: |
19 | java -version
20 | sudo apt-get update && sudo apt-get install openjdk-21-jdk
21 | sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java
22 | sudo update-alternatives --set javac /usr/lib/jvm/java-21-openjdk-amd64/bin/javac
23 | java -version
24 | export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
25 | - run:
26 | name: Generate Sources
27 | command: mvn web3j:generate-sources
28 | - run:
29 | name: Maven Tests
30 | command: mvn test
31 |
32 | orbs:
33 | maven: circleci/maven@1.4.1
34 |
35 | executors:
36 | machine_executor_amd64:
37 | machine:
38 | image: ubuntu-2204:2023.10.1
39 | environment:
40 | architecture: "amd64"
41 | platform: "linux/amd64"
42 |
43 | workflows:
44 | maven_test:
45 | jobs:
46 | - test
47 | - analyze:
48 | context: SonarCloud
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.project
2 | /.settings/
3 |
--------------------------------------------------------------------------------
/contract-service/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | /.classpath
3 | /.project
4 | /.settings/
5 | /UTC--*
6 |
--------------------------------------------------------------------------------
/contract-service/pom.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 | 4.0.0
6 |
7 | pl.piomin.services
8 | sample-spring-blockchain-contract
9 | 1.0-SNAPSHOT
10 |
11 | contract-service
12 | 1.0-SNAPSHOT
13 |
14 |
15 | ${project.artifactId}
16 |
17 |
18 |
19 |
20 | org.web3j
21 | web3j-spring-boot-starter
22 | 1.6.0
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-starter-web
27 |
28 |
29 | org.web3j
30 | core
31 | ${web3j.version}
32 |
33 |
34 | org.web3j
35 | tuples
36 | ${web3j.version}
37 |
38 |
39 | org.web3j
40 | abi
41 | ${web3j.version}
42 |
43 |
44 | org.web3j
45 | crypto
46 | ${web3j.version}
47 |
48 |
49 | org.springframework.boot
50 | spring-boot-starter-test
51 | test
52 |
53 |
54 | org.testcontainers
55 | testcontainers
56 | ${testcontainers.version}
57 | test
58 |
59 |
60 | org.testcontainers
61 | junit-jupiter
62 | ${testcontainers.version}
63 | test
64 |
65 |
66 |
67 |
68 |
69 |
70 | org.codehaus.mojo
71 | build-helper-maven-plugin
72 |
73 |
74 | add-source
75 | generate-sources
76 |
77 | add-source
78 |
79 |
80 |
81 | src/main/generated
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/contract-service/src/main/java/pl/piomin/services/contract/ContractApp.java:
--------------------------------------------------------------------------------
1 | package pl.piomin.services.contract;
2 |
3 | import javax.annotation.PostConstruct;
4 |
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.boot.SpringApplication;
9 | import org.springframework.boot.autoconfigure.SpringBootApplication;
10 | import org.web3j.protocol.Web3j;
11 |
12 | import pl.piomin.services.contract.service.ContractService;
13 |
14 | @SpringBootApplication
15 | public class ContractApp {
16 |
17 | private static final Logger LOGGER = LoggerFactory.getLogger(ContractApp.class);
18 |
19 | @Autowired
20 | Web3j web3j;
21 | @Autowired
22 | ContractService service;
23 |
24 | public static void main(String[] args) {
25 | SpringApplication.run(ContractApp.class, args);
26 | }
27 |
28 | @PostConstruct
29 | public void listen() {
30 | web3j.transactionFlowable().subscribe(tx -> {
31 | if (tx.getTo() != null && tx.getTo().equals(service.getOwnerAccount())) {
32 | LOGGER.info("New tx: id={}, block={}, from={}, to={}, value={}", tx.getHash(), tx.getBlockHash(), tx.getFrom(), tx.getTo(), tx.getValue().intValue());
33 | service.processContracts(tx.getValue().longValue());
34 | } else {
35 | LOGGER.info("Not matched: id={}, to={}", tx.getHash(), tx.getTo());
36 | }
37 | });
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/contract-service/src/main/java/pl/piomin/services/contract/controller/ContractController.java:
--------------------------------------------------------------------------------
1 | package pl.piomin.services.contract.controller;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.PostMapping;
6 | import org.springframework.web.bind.annotation.RequestBody;
7 | import org.springframework.web.bind.annotation.RequestMapping;
8 | import org.springframework.web.bind.annotation.RestController;
9 |
10 | import pl.piomin.services.contract.model.Contract;
11 | import pl.piomin.services.contract.service.ContractService;
12 |
13 | @RestController
14 | @RequestMapping("/contract")
15 | public class ContractController {
16 |
17 | @Autowired
18 | ContractService service;
19 |
20 | @GetMapping("/owner")
21 | public String getOwnerAccount() {
22 | return service.getOwnerAccount();
23 | }
24 |
25 | @PostMapping
26 | public Contract createContract(@RequestBody Contract newContract) throws Exception {
27 | return service.createContract(newContract);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/contract-service/src/main/java/pl/piomin/services/contract/model/Contract.java:
--------------------------------------------------------------------------------
1 | package pl.piomin.services.contract.model;
2 |
3 | public class Contract {
4 |
5 | private int fee;
6 | private String receiver;
7 | private String address;
8 |
9 | public int getFee() {
10 | return fee;
11 | }
12 |
13 | public void setFee(int fee) {
14 | this.fee = fee;
15 | }
16 |
17 | public String getReceiver() {
18 | return receiver;
19 | }
20 |
21 | public void setReceiver(String receiver) {
22 | this.receiver = receiver;
23 | }
24 |
25 | public String getAddress() {
26 | return address;
27 | }
28 |
29 | public void setAddress(String address) {
30 | this.address = address;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/contract-service/src/main/java/pl/piomin/services/contract/model/Transaction.java:
--------------------------------------------------------------------------------
1 | package pl.piomin.services.contract.model;
2 |
3 | public class Transaction {
4 |
5 | private String contract;
6 | private long amount;
7 |
8 | public String getContract() {
9 | return contract;
10 | }
11 |
12 | public void setContract(String contract) {
13 | this.contract = contract;
14 | }
15 |
16 | public long getAmount() {
17 | return amount;
18 | }
19 |
20 | public void setAmount(long amount) {
21 | this.amount = amount;
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/contract-service/src/main/java/pl/piomin/services/contract/service/ContractService.java:
--------------------------------------------------------------------------------
1 | package pl.piomin.services.contract.service;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.stereotype.Service;
7 | import org.web3j.crypto.CipherException;
8 | import org.web3j.crypto.Credentials;
9 | import org.web3j.crypto.WalletUtils;
10 | import org.web3j.protocol.Web3j;
11 | import org.web3j.protocol.core.DefaultBlockParameterName;
12 | import org.web3j.protocol.core.methods.request.EthFilter;
13 | import org.web3j.protocol.core.methods.request.Transaction;
14 | import org.web3j.protocol.core.methods.response.EthCoinbase;
15 | import org.web3j.protocol.core.methods.response.EthGetBalance;
16 | import org.web3j.protocol.core.methods.response.EthGetTransactionCount;
17 | import org.web3j.protocol.core.methods.response.TransactionReceipt;
18 | import pl.piomin.services.contract.model.Contract;
19 | import pl.piomin.services.contract.model.TransactionFee;
20 |
21 | import javax.annotation.PostConstruct;
22 | import java.io.IOException;
23 | import java.math.BigInteger;
24 | import java.security.InvalidAlgorithmParameterException;
25 | import java.security.NoSuchAlgorithmException;
26 | import java.security.NoSuchProviderException;
27 | import java.util.ArrayList;
28 | import java.util.List;
29 | import java.util.Optional;
30 |
31 | @Service
32 | public class ContractService {
33 |
34 | private static final Logger LOGGER = LoggerFactory.getLogger(ContractService.class);
35 | private static final String PASSWORD = "piot123";
36 | private static final BigInteger GAS_PRICE = BigInteger.valueOf(1L);
37 | private static final BigInteger GAS_LIMIT = BigInteger.valueOf(500_000L);
38 |
39 | @Autowired
40 | Web3j web3j;
41 | Credentials credentials;
42 | private List contracts = new ArrayList<>();
43 |
44 | @PostConstruct
45 | public void init() throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, CipherException {
46 | String file = WalletUtils.generateLightNewWalletFile(PASSWORD, null);
47 | credentials = WalletUtils.loadCredentials(PASSWORD, file);
48 | LOGGER.info("Credentials created: file={}, address={}", file, credentials.getAddress());
49 | EthCoinbase coinbase = web3j.ethCoinbase().send();
50 | EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(coinbase.getAddress(), DefaultBlockParameterName.LATEST).send();
51 | Transaction transaction = Transaction.createEtherTransaction(coinbase.getAddress(), transactionCount.getTransactionCount(), BigInteger.valueOf(20_000_000_000L), BigInteger.valueOf(21_000), credentials.getAddress(), BigInteger.valueOf(25_000_000_000_000_000L));
52 | web3j.ethSendTransaction(transaction).send();
53 | EthGetBalance balance = web3j.ethGetBalance(credentials.getAddress(), DefaultBlockParameterName.LATEST).send();
54 | LOGGER.info("Balance: {}", balance.getBalance().longValue());
55 | }
56 |
57 | public String getOwnerAccount() {
58 | return credentials.getAddress();
59 | }
60 |
61 | public Contract createContract(Contract newContract) throws Exception {
62 | String file = WalletUtils.generateLightNewWalletFile(PASSWORD, null);
63 | Credentials receiverCredentials = WalletUtils.loadCredentials(PASSWORD, file);
64 | LOGGER.info("Credentials created: file={}, address={}", file, credentials.getAddress());
65 | TransactionFee contract = TransactionFee.deploy(web3j, credentials, GAS_PRICE, GAS_LIMIT, receiverCredentials.getAddress(), BigInteger.valueOf(newContract.getFee())).send();
66 | newContract.setReceiver(receiverCredentials.getAddress());
67 | newContract.setAddress(contract.getContractAddress());
68 | contracts.add(contract.getContractAddress());
69 | LOGGER.info("New contract deployed: address={}", contract.getContractAddress());
70 | Optional tr = contract.getTransactionReceipt();
71 | if (tr.isPresent()) {
72 | LOGGER.info("Transaction receipt: from={}, to={}, gas={}", tr.get().getFrom(), tr.get().getTo(), tr.get().getGasUsed().intValue());
73 | }
74 | return newContract;
75 | }
76 |
77 | public void processContracts(long transactionAmount) {
78 | contracts.forEach(it -> {
79 | TransactionFee contract = TransactionFee.load(it, web3j, credentials, GAS_PRICE, GAS_LIMIT);
80 | try {
81 | TransactionReceipt tr = contract.sendTrx(BigInteger.valueOf(transactionAmount)).send();
82 | LOGGER.info("Transaction receipt: from={}, to={}, gas={}", tr.getFrom(), tr.getTo(), tr.getGasUsed().intValue());
83 | LOGGER.info("Get receiver: {}", contract.getReceiverBalance().send().longValue());
84 | EthFilter filter = new EthFilter(DefaultBlockParameterName.EARLIEST, DefaultBlockParameterName.LATEST, contract.getContractAddress());
85 | web3j.ethLogFlowable(filter).subscribe(log -> {
86 | LOGGER.info("Log: {}", log.getData());
87 | });
88 | } catch (Exception e) {
89 | LOGGER.error("Error during contract execution", e);
90 | }
91 | });
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/contract-service/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: contract-service
4 | server:
5 | port: ${PORT:8090}
6 | web3j:
7 | client-address: http://localhost:8545
--------------------------------------------------------------------------------
/contract-service/src/main/resources/fee.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.4.21;
2 |
3 | contract TransactionFee {
4 |
5 | // (1)
6 | uint public fee;
7 | // (2)
8 | address public receiver;
9 | // (3)
10 | mapping (address => uint) public balances;
11 | // (4)
12 | event Sent(address from, address to, uint amount, bool sent);
13 |
14 | // (5)
15 | constructor(address _receiver, uint _fee) public {
16 | receiver = _receiver;
17 | fee = _fee;
18 | }
19 |
20 | // (6)
21 | function getReceiverBalance() public view returns(uint) {
22 | return receiver.balance;
23 | }
24 |
25 | // (7)
26 | function sendTrx() public payable {
27 | uint value = msg.value * fee / 100;
28 | bool sent = receiver.send(value);
29 | balances[receiver] += (value);
30 | emit Sent(msg.sender, receiver, value, sent);
31 | }
32 |
33 | }
--------------------------------------------------------------------------------
/contract-service/src/test/java/pl/piomin/services/contract/ContractTest.java:
--------------------------------------------------------------------------------
1 | package pl.piomin.services.contract;
2 |
3 | import org.junit.jupiter.api.BeforeEach;
4 | import org.junit.jupiter.api.Test;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.boot.test.context.SpringBootTest;
9 | import org.springframework.test.context.DynamicPropertyRegistry;
10 | import org.springframework.test.context.DynamicPropertySource;
11 | import org.testcontainers.containers.GenericContainer;
12 | import org.testcontainers.junit.jupiter.Container;
13 | import org.testcontainers.junit.jupiter.Testcontainers;
14 | import org.web3j.crypto.Credentials;
15 | import org.web3j.crypto.WalletUtils;
16 | import org.web3j.protocol.Web3j;
17 | import org.web3j.protocol.core.DefaultBlockParameterName;
18 | import org.web3j.protocol.core.methods.request.EthFilter;
19 | import org.web3j.protocol.core.methods.request.Transaction;
20 | import org.web3j.protocol.core.methods.response.EthCoinbase;
21 | import org.web3j.protocol.core.methods.response.EthGetBalance;
22 | import org.web3j.protocol.core.methods.response.EthGetTransactionCount;
23 | import org.web3j.protocol.core.methods.response.TransactionReceipt;
24 | import org.web3j.tx.RawTransactionManager;
25 | import org.web3j.tx.TransactionManager;
26 | import pl.piomin.services.contract.model.TransactionFee;
27 |
28 | import java.math.BigInteger;
29 |
30 | @SpringBootTest
31 | @Testcontainers
32 | public class ContractTest {
33 |
34 | private static final Logger LOGGER = LoggerFactory.getLogger(ContractTest.class);
35 | public static final BigInteger GAS_PRICE = BigInteger.valueOf(1L);
36 | public static final BigInteger GAS_LIMIT = BigInteger.valueOf(500_000L);
37 | public static final BigInteger AMOUNT = BigInteger.valueOf(10_000L);
38 | public static final BigInteger FEE = BigInteger.valueOf(10L);
39 |
40 | @Autowired
41 | Web3j web3j;
42 | Credentials credentialsFrom;
43 | Credentials credentialsTo;
44 |
45 | @Container
46 | static final GenericContainer clientEthereum = new GenericContainer("ethereum/client-go")
47 | .withCommand("--http", "--http.corsdomain=*", "--http.addr=0.0.0.0", "--dev")
48 | .withExposedPorts(8545);
49 |
50 | @DynamicPropertySource
51 | static void registerCeProperties(DynamicPropertyRegistry registry) {
52 | registry.add("web3j.client-address",
53 | () -> String.format("http://localhost:%d", clientEthereum.getFirstMappedPort()));
54 | }
55 |
56 | @BeforeEach
57 | public void before() throws Exception {
58 | // String file = WalletUtils.generateLightNewWalletFile("piot123", null);
59 | // String file = "src/test/resources/john_smith.json";
60 | credentialsFrom = WalletUtils.loadCredentials("piot123", "src/test/resources/john_smith.json");
61 | credentialsTo = WalletUtils.loadCredentials("piot123", "src/test/resources/jason_sullivan.json");
62 | LOGGER.info("Credentials: address={}", credentialsFrom.getAddress());
63 | EthCoinbase coinbase = web3j.ethCoinbase().send();
64 | EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(coinbase.getAddress(), DefaultBlockParameterName.LATEST).send();
65 | Transaction transaction = Transaction.createEtherTransaction(coinbase.getAddress(), transactionCount.getTransactionCount(), BigInteger.valueOf(20_000_000_000L), BigInteger.valueOf(21_000), credentialsFrom.getAddress(), BigInteger.valueOf(25_000_000_000_000_000L));
66 | web3j.ethSendTransaction(transaction).send();
67 | EthGetBalance balance = web3j.ethGetBalance(credentialsFrom.getAddress(), DefaultBlockParameterName.LATEST).send();
68 | LOGGER.info("Balance: {}", balance.getBalance().longValue());
69 | }
70 |
71 | @Test
72 | public void testTransactionFeeContract() throws Exception {
73 | long chainId = 56;
74 | TransactionManager txManager = new RawTransactionManager(web3j, credentialsFrom, chainId);
75 | TransactionFee contract = TransactionFee.deploy(web3j, credentialsFrom, GAS_PRICE, GAS_LIMIT, "0xd7850bd94f189ce38ce5729052926094997de310", FEE).send();
76 | EthGetBalance balance = web3j.ethGetBalance(credentialsTo.getAddress(), DefaultBlockParameterName.LATEST).send();
77 | EthFilter filter = new EthFilter(DefaultBlockParameterName.EARLIEST, DefaultBlockParameterName.LATEST, contract.getContractAddress());
78 | LOGGER.info("Sending to: account={}, balance={}", "0xd7850bd94f189ce38ce5729052926094997de310", balance.getBalance().longValue());
79 | for (int i = 0; i < 3; i++) {
80 | TransactionReceipt tr = contract.sendTrx(AMOUNT).send();
81 | LOGGER.info("Transaction receipt: from={}, to={}, gas={}", tr.getFrom(), tr.getTo(), tr.getGasUsed().intValue());
82 | balance = web3j.ethGetBalance(credentialsFrom.getAddress(), DefaultBlockParameterName.LATEST).send();
83 | LOGGER.info("From balance after: account={}, balance={}", "0xd7850bd94f189ce38ce5729052926094997de310", balance.getBalance().longValue());
84 | balance = web3j.ethGetBalance(credentialsTo.getAddress(), DefaultBlockParameterName.LATEST).send();
85 | LOGGER.info("To balance after: account={}, balance={}", "0xd7850bd94f189ce38ce5729052926094997de310", balance.getBalance().longValue());
86 | LOGGER.info("Contract: account={}, balance={}", "0xd7850bd94f189ce38ce5729052926094997de310", contract.balances(credentialsTo.getAddress()).send().longValue());
87 | LOGGER.info("Get receiver: {}", contract.getReceiverBalance().send().longValue());
88 | // LOGGER.info("Contract To: account={}, balance={}", tr.getTo(), contract.balances(tr.getTo()).send().longValue());
89 | // balance = web3j.ethGetBalance(tr.getTo(), DefaultBlockParameterName.LATEST).send();
90 | // LOGGER.info("Contract To 2: account={}, balance={}", credentialsTo.getAddress(), balance.getBalance().longValue());
91 | }
92 |
93 | web3j.ethLogFlowable(filter).subscribe(log -> {
94 | LOGGER.info("Log: {}", log.getData());
95 | });
96 | Thread.sleep(2000);
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/contract-service/src/test/resources/jason_sullivan.json:
--------------------------------------------------------------------------------
1 | {"address":"a40c8397390c48b56d077f334ff7aeaff36374c4","id":"b7239298-f945-4a59-972e-37b384475bc4","version":3,"crypto":{"cipher":"aes-128-ctr","ciphertext":"994a8840090f66df0c072c31f9fce4a672a5ad52ba1fddbf8548cdee5f9b4cc6","cipherparams":{"iv":"5df27bd988c11f59b44682e8237498c2"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"4551fb61fa1a30d2d841d4fa5a489c10b9d879ddc93cfe2f9950519785b68ba5"},"mac":"5e9bd229416852c241e04020196ab517e6feb02e26d932d0753e424c03724b6f"}}
--------------------------------------------------------------------------------
/contract-service/src/test/resources/john_smith.json:
--------------------------------------------------------------------------------
1 | {"address":"d23d1f3206e8e92cdc3d4435517f16eb9a780da1","id":"ecf83ed5-0cea-4625-bd38-fc54a10c26ef","version":3,"crypto":{"cipher":"aes-128-ctr","ciphertext":"437b566ed9cae7675cf3b366517ed18d55e1e15157d2f1a16af25e12fa6cc6ae","cipherparams":{"iv":"b724434a3dab1359e1073aeacfce3228"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"cb91df23d6a29eea6b470af7583445a6fc911b55f083375364144fefc0ca73be"},"mac":"9b018dfa08642172472a887b5064be2d7f43f2cbef3d369348c18ca7e87f7393"}}
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.7.18
9 |
10 |
11 | pl.piomin.services
12 | sample-spring-blockchain-contract
13 | 1.0-SNAPSHOT
14 | pom
15 |
16 |
17 | 11
18 | piomin_sample-spring-blockchain-contract
19 | piomin
20 | https://sonarcloud.io
21 | 1.19.6
22 | 4.10.0
23 |
24 |
25 |
26 | contract-service
27 | transaction-service
28 |
29 |
30 |
31 |
32 |
33 | org.web3j
34 | web3j-maven-plugin
35 | 4.11.0
36 |
37 | pl.piomin.services.contract.model
38 | contract-service/src/main/generated
39 |
40 | contract-service/src/main/resources
41 |
42 | **/*.sol
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## Intro to Blockchain with Ethereum, Web3j and Spring Boot: Smart Contracts [](https://twitter.com/piotr_minkowski)
2 |
3 | Detailed description can be found here: [Intro to Blockchain with Ethereum, Web3j and Spring Boot: Smart Contracts](https://piotrminkowski.com/2018/07/25/intro-to-blockchain-with-ethereum-web3j-and-spring-boot-smart-contracts/)
4 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base",":dependencyDashboard"
5 | ],
6 | "packageRules": [
7 | {
8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
9 | "automerge": true
10 | }
11 | ],
12 | "prCreation": "not-pending"
13 | }
--------------------------------------------------------------------------------
/transaction-service/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | /.classpath
3 | /.project
4 | /.settings/
5 |
--------------------------------------------------------------------------------
/transaction-service/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 | pl.piomin.services
6 | sample-spring-blockchain-contract
7 | 1.0-SNAPSHOT
8 |
9 | transaction-service
10 | pl.piomin.services
11 | 1.0-SNAPSHOT
12 |
13 |
14 | ${project.artifactId}
15 |
16 |
17 |
18 |
19 | org.web3j
20 | web3j-spring-boot-starter
21 | 1.6.0
22 |
23 |
24 | org.springframework.boot
25 | spring-boot-starter-web
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter-test
30 | test
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/transaction-service/src/main/java/pl/piomin/services/transaction/TransactionApp.java:
--------------------------------------------------------------------------------
1 | package pl.piomin.services.transaction;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.web.client.RestTemplate;
7 |
8 | @SpringBootApplication
9 | public class TransactionApp {
10 |
11 | public static void main(String[] args) {
12 | SpringApplication.run(TransactionApp.class, args);
13 | }
14 |
15 | @Bean
16 | RestTemplate rest() {
17 | return new RestTemplate();
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/transaction-service/src/main/java/pl/piomin/services/transaction/controller/TransactionController.java:
--------------------------------------------------------------------------------
1 | package pl.piomin.services.transaction.controller;
2 |
3 | import java.io.IOException;
4 | import java.math.BigInteger;
5 | import java.security.InvalidAlgorithmParameterException;
6 | import java.security.NoSuchAlgorithmException;
7 | import java.security.NoSuchProviderException;
8 |
9 | import javax.annotation.PostConstruct;
10 |
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 | import org.springframework.beans.factory.annotation.Autowired;
14 | import org.springframework.beans.factory.annotation.Value;
15 | import org.springframework.web.bind.annotation.PostMapping;
16 | import org.springframework.web.bind.annotation.RequestBody;
17 | import org.springframework.web.bind.annotation.RequestMapping;
18 | import org.springframework.web.bind.annotation.RestController;
19 | import org.springframework.web.client.RestTemplate;
20 | import org.web3j.crypto.CipherException;
21 | import org.web3j.crypto.Credentials;
22 | import org.web3j.protocol.Web3j;
23 | import org.web3j.protocol.core.DefaultBlockParameterName;
24 | import org.web3j.protocol.core.methods.request.Transaction;
25 | import org.web3j.protocol.core.methods.response.EthAccounts;
26 | import org.web3j.protocol.core.methods.response.EthCoinbase;
27 | import org.web3j.protocol.core.methods.response.EthGetBalance;
28 | import org.web3j.protocol.core.methods.response.EthGetTransactionCount;
29 | import org.web3j.protocol.core.methods.response.EthGetTransactionReceipt;
30 | import org.web3j.protocol.core.methods.response.EthSendTransaction;
31 | import org.web3j.protocol.core.methods.response.TransactionReceipt;
32 |
33 | import pl.piomin.services.transaction.model.TransactionRequest;
34 |
35 | @RestController
36 | @RequestMapping("/transaction")
37 | public class TransactionController {
38 |
39 | private static final Logger LOGGER = LoggerFactory.getLogger(TransactionController.class);
40 | public static final BigInteger GAS_PRICE = BigInteger.valueOf(1L);
41 | public static final BigInteger GAS_LIMIT = BigInteger.valueOf(21_000L);
42 |
43 | @Value("${contract-service.url}")
44 | String url;
45 | @Autowired
46 | Web3j web3j;
47 | @Autowired
48 | RestTemplate template;
49 | Credentials credentials;
50 |
51 | @PostConstruct
52 | public void init() throws IOException, CipherException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
53 | EthCoinbase coinbase = web3j.ethCoinbase().send();
54 | EthAccounts accounts = web3j.ethAccounts().send();
55 | for (int i = 1; i < accounts.getAccounts().size(); i++) {
56 | EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(coinbase.getAddress(), DefaultBlockParameterName.LATEST).send();
57 | Transaction transaction = Transaction.createEtherTransaction(coinbase.getAddress(), transactionCount.getTransactionCount(), GAS_PRICE, GAS_LIMIT, accounts.getAccounts().get(i), BigInteger.valueOf(25_000_000_000L));
58 | web3j.ethSendTransaction(transaction).send();
59 | EthGetBalance balance = web3j.ethGetBalance(accounts.getAccounts().get(i), DefaultBlockParameterName.LATEST).send();
60 | LOGGER.info("Balance: address={}, amount={}", accounts.getAccounts().get(i), balance.getBalance().longValue());
61 | }
62 | }
63 |
64 | @PostMapping
65 | public String performTransaction(@RequestBody TransactionRequest request) throws Exception {
66 | EthAccounts accounts = web3j.ethAccounts().send();
67 | String owner = template.getForObject(url, String.class);
68 | EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(accounts.getAccounts().get(request.getFromId()), DefaultBlockParameterName.LATEST).send();
69 | Transaction transaction = Transaction.createEtherTransaction(accounts.getAccounts().get(request.getFromId()), transactionCount.getTransactionCount(), GAS_PRICE, GAS_LIMIT, owner, BigInteger.valueOf(request.getAmount()));
70 | EthSendTransaction response = web3j.ethSendTransaction(transaction).send();
71 | if (response.getError() != null) {
72 | LOGGER.error("Transaction error: {}", response.getError().getMessage());
73 | return "ERR";
74 | }
75 | LOGGER.info("Transaction: {}", response.getResult());
76 | EthGetTransactionReceipt receipt = web3j.ethGetTransactionReceipt(response.getTransactionHash()).send();
77 | if (receipt.getTransactionReceipt().isPresent()) {
78 | TransactionReceipt r = receipt.getTransactionReceipt().get();
79 | LOGGER.info("Tx receipt: from={}, to={}, gas={}, cumulativeGas={}", r.getFrom(), r.getTo(), r.getGasUsed().intValue(), r.getCumulativeGasUsed().intValue());
80 | }
81 | EthGetBalance balance = web3j.ethGetBalance(accounts.getAccounts().get(request.getFromId()), DefaultBlockParameterName.LATEST).send();
82 | LOGGER.info("Balance: address={}, amount={}", accounts.getAccounts().get(request.getFromId()), balance.getBalance().longValue());
83 | balance = web3j.ethGetBalance(owner, DefaultBlockParameterName.LATEST).send();
84 | LOGGER.info("Balance: address={}, amount={}", owner, balance.getBalance().longValue());
85 | return response.getTransactionHash();
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/transaction-service/src/main/java/pl/piomin/services/transaction/model/TransactionRequest.java:
--------------------------------------------------------------------------------
1 | package pl.piomin.services.transaction.model;
2 |
3 | public class TransactionRequest {
4 |
5 | private int fromId;
6 | private int toId;
7 | private long amount;
8 |
9 | public int getFromId() {
10 | return fromId;
11 | }
12 |
13 | public void setFromId(int fromId) {
14 | this.fromId = fromId;
15 | }
16 |
17 | public int getToId() {
18 | return toId;
19 | }
20 |
21 | public void setToId(int toId) {
22 | this.toId = toId;
23 | }
24 |
25 | public long getAmount() {
26 | return amount;
27 | }
28 |
29 | public void setAmount(long amount) {
30 | this.amount = amount;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/transaction-service/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: transaction-service
4 | server:
5 | port: ${PORT:8091}
6 | web3j:
7 | client-address: http://localhost:8545
8 |
9 | contract-service:
10 | url: http://localhost:8090/contract/owner
--------------------------------------------------------------------------------