├── .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 [![Twitter](https://img.shields.io/twitter/follow/piotr_minkowski.svg?style=social&logo=twitter&label=Follow%20Me)](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 --------------------------------------------------------------------------------