-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] 스마트 컨트랙트 환불 메소드 호출하는 서버 스케줄러 작성 #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 11 commits
3fbf175
12879c8
03ea920
34dd6b6
7f80a64
37f9d91
dbd8bf0
9964f7a
99e5458
5327622
3aa54f0
c91986b
6470df4
a4203be
17cce40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.donet.donet.donation.adapter.in.scheduler; | ||
|
|
||
| import com.donet.donet.donation.application.port.in.RefundDonationUsecase; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.scheduling.annotation.Scheduled; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class RefundScheduler { | ||
| private final RefundDonationUsecase refundDonationUsecase; | ||
|
|
||
| @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") | ||
| public void runRefundJob() { | ||
| refundDonationUsecase.refundDonation(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.donet.donet.donation.application; | ||
|
|
||
| import com.donet.donet.donation.application.port.in.RefundDonationUsecase; | ||
| import com.donet.donet.donation.application.port.out.FindDonationPort; | ||
| import com.donet.donet.donation.application.port.out.SmartContractPort; | ||
| import com.donet.donet.donation.domain.Donation; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class RefundDonationService implements RefundDonationUsecase { | ||
|
|
||
| private final FindDonationPort findDonationPort; | ||
| private final SmartContractPort smartContractPort; | ||
|
|
||
| @Override | ||
| public void refundDonation() { | ||
| List<Long> refundTargetDonationIds = findDonationPort.findRefundableDonation() | ||
| .stream() | ||
| .map(Donation::getId) | ||
| .toList(); | ||
|
Comment on lines
+23
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain예외 처리 전략을 검증하세요.
다음을 확인하세요:
다음 스크립트로 스케줄러의 예외 처리 구현을 확인하세요: 🏁 Script executed: #!/bin/bash
# Description: 스케줄러에서 예외 처리가 구현되어 있는지 확인
# RefundScheduler 파일 찾기 및 예외 처리 로직 검색
fd -e java RefundScheduler --exec cat {} \; | grep -A 10 -B 5 "refundDonation"Length of output: 327 스케줄러 레벨의 예외 처리를 더 자세히 검증하기 위해 RefundDonationService 구현을 확인하겠습니다. Length of output: 1390 🏁 Script executed: #!/bin/bash
# RefundDonationUsecase 인터페이스 확인
fd -e java RefundDonationUsecase --exec cat {} \;Length of output: 124 🏁 Script executed: #!/bin/bash
# 전체 프로젝트에서 refundDonation 메소드 구현 찾기
rg -n "refundDonation\s*\(" --type java -A 15Length of output: 2399 🏁 Script executed: #!/bin/bash
# Spring @Scheduled에 대한 에러 핸들링 설정 확인
rg -n "TaskScheduler\|SchedulingConfigurer\|@EnableScheduling" --type java -B 2 -A 5Length of output: 0 추가로 프로젝트 레벨의 글로벌 에러 핸들링 설정을 확인하겠습니다. Length of output: 0 🏁 Script executed: #!/bin/bash
# 프로젝트 루트의 application 설정 파일 확인
fd -e yml -e yaml -e properties | head -20 | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'Length of output: 3726 🏁 Script executed: #!/bin/bash
# FindDonationPort 포트의 구현체 확인 - findRefundableDonation 메소드
rg -n "findRefundableDonation" --type java -B 5 -A 10Length of output: 6038
검증 결과, RefundDonationService의 23-26번 줄에서 데이터베이스 조회 메소드를 호출할 때 예외 처리가 없습니다. DB 연결 실패, 타임아웃 등의 예외가 발생하면 스케줄러로 전파되어 실행 흐름이 중단됩니다. Spring @scheduled는 예외를 로깅하고 다음 스케줄에 계속 실행되지만, 애플리케이션 레벨의 명시적인 예외 처리 전략이 부재합니다. 다음과 같이 예외 처리를 추가하세요:
🤖 Prompt for AI Agents |
||
|
|
||
| smartContractPort.refundDonations(refundTargetDonationIds); | ||
| } | ||
|
Comment on lines
21
to
41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스마트 컨트랙트 호출 결과를 처리하세요. Line 26에서 다음을 추가하세요:
다음과 같이 개선할 수 있습니다: +import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
@Service
@RequiredArgsConstructor
public class RefundDonationService implements RefundDonationUsecase {
private final FindDonationPort findDonationPort;
private final SmartContractPort smartContractPort;
@Override
public void refundDonation() {
List<Long> refundTargetDonationIds = findDonationPort.findRefundableDonation()
.stream()
.map(Donation::getId)
.toList();
- smartContractPort.refundDonations(refundTargetDonationIds);
+ if (refundTargetDonationIds.isEmpty()) {
+ log.info("환불 대상 기부가 없습니다.");
+ return;
+ }
+
+ log.info("환불 처리 시작: {} 건", refundTargetDonationIds.size());
+ boolean success = smartContractPort.refundDonations(refundTargetDonationIds);
+
+ if (success) {
+ log.info("환불 처리 완료: {} 건", refundTargetDonationIds.size());
+ } else {
+ log.error("환불 처리 실패: donation IDs = {}", refundTargetDonationIds);
+ // TODO: 알림 또는 재시도 로직 추가 고려
+ }
}
}
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.donet.donet.donation.application.port.in; | ||
|
|
||
| public interface RefundDonationUsecase { | ||
| void refundDonation(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.donet.donet.donation.application.port.out; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface SmartContractPort { | ||
| boolean refundDonations(List<Long> donationIds); | ||
| } |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package com.donet.donet.global.smartContract; | ||
|
|
||
| import com.donet.donet.donation.application.port.out.SmartContractPort; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import org.web3j.protocol.Web3j; | ||
| import org.web3j.crypto.Credentials; | ||
| import org.web3j.protocol.core.methods.response.TransactionReceipt; | ||
| import org.web3j.tx.gas.ContractGasProvider; | ||
|
|
||
| import java.math.BigInteger; | ||
| import java.util.List; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class SmartContractAdapter implements SmartContractPort { | ||
|
|
||
| private final CampaignFactory campaignFactory; // 컨트랙트 주소 조회용 | ||
| private final Web3j web3j; // Web3 네트워크 연동 | ||
| private final Credentials credentials; // 서버 지갑 | ||
| private final ContractGasProvider gasProvider; // 가스 설정 | ||
|
|
||
| @Override | ||
| public boolean refundDonations(List<Long> donationIds) { | ||
|
|
||
| try { | ||
| for (Long donationId : donationIds) { | ||
|
|
||
| // 1. Factory에서 donationId로 캠페인 주소 조회 | ||
| String campaignAddress = String.valueOf(campaignFactory.getCampaignAddress(BigInteger.valueOf(donationId))); | ||
|
|
||
| if (campaignAddress == null) { | ||
| throw new IllegalStateException("Campaign address not found for donationId=" + donationId); | ||
| } | ||
|
|
||
| // 2. 해당 주소로 Campaign 컨트랙트 인스턴스 로딩 | ||
| Campaign campaign = Campaign.load( | ||
| campaignAddress, | ||
| web3j, | ||
| credentials, | ||
| gasProvider | ||
| ); | ||
|
|
||
| // 3. Donation ID 기반 환불 트랜잭션 실행 | ||
| TransactionReceipt tx = campaign.refund().send(); | ||
|
|
||
| // 4. 실패 여부 확인 | ||
| if (!tx.isStatusOK()) { | ||
| throw new RuntimeException("Refund transaction failed: " + tx.getTransactionHash()); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| System.out.println("Refund success for donationId=" + donationId + | ||
| " / tx=" + tx.getTransactionHash()); | ||
| } | ||
|
|
||
| return true; | ||
|
|
||
| } catch (Exception e) { | ||
| e.printStackTrace(); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package com.donet.donet.global.smartContract; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.web3j.crypto.Credentials; | ||
| import org.web3j.protocol.Web3j; | ||
| import org.web3j.protocol.http.HttpService; | ||
| import org.web3j.tx.gas.ContractGasProvider; | ||
| import org.web3j.tx.gas.StaticGasProvider; | ||
|
|
||
| import java.math.BigInteger; | ||
|
|
||
| @Configuration | ||
| @RequiredArgsConstructor | ||
| public class Web3jConfig { | ||
|
|
||
| @Value("${web3.rpcUrl}") | ||
| private String rpcUrl; | ||
|
|
||
| @Value("${web3.privateKey}") | ||
| private String privateKey; | ||
|
|
||
| @Value("${web3.gasPrice}") | ||
| private BigInteger gasPrice; | ||
|
|
||
| @Value("${web3.gasLimit}") | ||
| private BigInteger gasLimit; | ||
|
|
||
| // 이미 배포되어 있는 CampaignFactory의 on-chain 주소 | ||
| @Value("${contracts.campaignFactoryAddress}") | ||
| private String campaignFactoryAddress; | ||
|
Comment on lines
+21
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain설정 값 검증 및 에러 처리 필요 PR 설명에서 언급한 대로 현재 환경 변수가 비어있어 런타임 에러가 발생할 수 있습니다. 빈 설정 또는 잘못된 형식의 값이 주입될 경우를 대비한 검증 로직을 추가하는 것을 권장합니다. 특히 다음 사항을 확인하세요:
다음 스크립트로 애플리케이션이 시작될 때 설정 검증이 제대로 이루어지는지 확인하세요: 🏁 Script executed: #!/bin/bash
# 설정 검증 로직이 있는지 확인
rg -n "privateKey|rpcUrl|campaignFactoryAddress" --type java -A 5 -B 5 -g '!**/test/**'Length of output: 4046 Length of output: 1624 설정 값 검증 및 에러 처리 필수 현재 Web3jConfig.java의 설정 값들이 검증 없이 직접 사용되고 있습니다. 빈 또는 잘못된 형식의 설정이 주입될 경우 애플리케이션 시작 시점에 실패합니다. 다음 위치에서 검증 로직 추가가 필요합니다:
추가로 🤖 Prompt for AI Agents |
||
|
|
||
| @Bean | ||
| public Web3j web3j() { | ||
| return Web3j.build(new HttpService(rpcUrl)); | ||
| } | ||
|
|
||
| @Bean | ||
| public Credentials credentials() { | ||
| return Credentials.create(privateKey); | ||
| } | ||
|
Comment on lines
+43
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainPrivate key 보안 및 유효성 검증 필요
보안 주의사항:
다음 스크립트로 private key가 로그에 노출되는지 확인하세요: 🏁 Script executed: #!/bin/bash
# Private key 로깅 여부 확인
rg -n "privateKey.*log|log.*privateKey|System\.out.*privateKey" --type java -g '!**/test/**'Length of output: 0 더 정확한 검증을 위해 추가 정보가 필요합니다. Length of output: 1624 🏁 Script executed: #!/bin/bash
# privateKey 필드 및 초기화 방식 확인
rg -n "privateKey" --type java -g '!**/test/**' -B5 -A5 | head -100Length of output: 2167 🏁 Script executed: #!/bin/bash
# 로깅 관련 설정 파일 확인
find . -name "logback*.xml" -o -name "log4j*.xml" -o -name "log4j*.properties" | head -20Length of output: 0 🏁 Script executed: #!/bin/bash
# 예외 처리 패턴 및 스택 트레이스 로깅 확인
rg -n "catch.*Exception|printStackTrace|logger.*Exception" --type java -g '!**/test/**' -C2 | head -50Length of output: 5283 🏁 Script executed: #!/bin/bash
# application.yml/properties 파일에서 privateKey 설정 방식 확인
find . -name "application*.yml" -o -name "application*.properties" | xargs cat 2>/dev/nullLength of output: 2685 🏁 Script executed: #!/bin/bash
# pom.xml에서 web3j 버전 확인
find . -name "pom.xml" -exec cat {} \; | grep -A5 "web3j"Length of output: 0 🏁 Script executed: #!/bin/bash
# web3j Credentials.create() 동작 확인을 위한 웹 검색 준비
echo "web3j Credentials.create validation"Length of output: 35 추가 검증이 필요합니다. 🏁 Script executed: #!/bin/bash
# pom.xml 재확인 - 전체 내용
cat pom.xml 2>/dev/null | head -200Length of output: 0 🏁 Script executed: #!/bin/bash
# Credentials 관련 예외 처리 확인
rg -n "Credentials\." --type java -g '!**/test/**' -B3 -A3Length of output: 610 🏁 Script executed: #!/bin/bash
# 빈 초기화 관련 에러 로깅 확인
rg -n "InitializationException|BeanInstantiation|BeanCreation" --type java -g '!**/test/**'Length of output: 0 🏁 Script executed: #!/bin/bash
# SmartContractAdapter 전체 내용 확인
find . -name "SmartContractAdapter.java" -type f ! -path "*/test/*" -exec cat {} \;Length of output: 2243 🌐 Web query: 💡 Result: Short answer: Credentials.create expects a 32-byte private key encoded as 64 hex characters (0x prefix optional). Common failures come from non-hex chars, wrong length (e.g. 63 chars) or passing a decimal string — web3j will throw NumberFormatException / IllegalArgumentException when it can't parse the key. See docs. [1] How to handle it
Example (concise): Notes:
Sources: web3j Credentials docs and community discussion about 63/64-char keys and padding. [1][2] References Private key 유효성 검증 및 예외 처리 필수
문제점:
권장사항:
private String normalizePrivateKey(String pk) {
if (pk == null || pk.isEmpty()) {
throw new IllegalArgumentException("privateKey must not be empty");
}
pk = pk.startsWith("0x") ? pk.substring(2) : pk;
if (!pk.matches("[0-9a-fA-F]+")) {
throw new IllegalArgumentException("privateKey must be hexadecimal");
}
if (pk.length() != 64) {
throw new IllegalArgumentException("privateKey must be 64 hex characters");
}
return pk;
}
@Bean
public Credentials credentials() {
try {
return Credentials.create(normalizePrivateKey(privateKey));
} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid privateKey configuration", e);
}
}🤖 Prompt for AI Agents |
||
|
|
||
| @Bean | ||
| public ContractGasProvider contractGasProvider() { | ||
| return new StaticGasProvider(gasPrice, gasLimit); | ||
| } | ||
|
|
||
| @Bean | ||
| public CampaignFactory campaignFactory( | ||
| Web3j web3j, | ||
| Credentials credentials, | ||
| ContractGasProvider gasProvider | ||
| ) { | ||
| return CampaignFactory.load(campaignFactoryAddress, web3j, credentials, gasProvider); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.