Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ dependencies {
implementation 'software.amazon.awssdk:s3'

// Smart Contract
implementation 'org.web3j:core:4.9.4'
implementation 'org.web3j:core:4.12.0'
implementation 'org.web3j:contracts:4.12.0'
}

tasks.named('test') {
Expand Down
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
Expand Up @@ -26,6 +26,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -125,6 +126,14 @@ public List<RegisteredDonation> findRegisteredDonations(User user, int size) {
return registeredDonations;
}

@Override
public List<Donation> findRefundableDonation() {
return donationRepository.findRefundableDonations(LocalDate.now())
.stream()
.map(donationMapper::mapToDomainEntity)
.toList();
}

@Override
public Donation increaseDonationView(Long donationId) {
DonationJpaEntity donationJpaEntity = donationRepository.findDonationById(donationId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -90,4 +91,12 @@ SELECT COUNT(*)
Page<JoinedDonationProjection> findJoinedDonations(@Param("userId") Long userId, Pageable pageable);

List<DonationJpaEntity> findAllByUserJpaEntityOrderByIdDesc(UserJpaEntity userJpaEntity, Pageable pageable);

@Query("""
SELECT d FROM DonationJpaEntity d
WHERE d.endDate < :today
AND d.currentAmount < d.targetAmount
AND d.status = 'ACTIVE'
""")
List<DonationJpaEntity> findRefundableDonations(LocalDate today);
}
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

예외 처리 전략을 검증하세요.

findDonationPort.findRefundableDonation() 호출 시 발생할 수 있는 예외(DB 연결 실패, 타임아웃 등)가 처리되지 않고 있습니다. 스케줄러가 예외로 인해 중단될 수 있으며, 이후 실행에도 영향을 줄 수 있습니다.

다음을 확인하세요:

  • 스케줄러 레벨에서 예외를 처리하는지 검증
  • 또는 이 메소드에 try-catch 추가 고려

다음 스크립트로 스케줄러의 예외 처리 구현을 확인하세요:


🏁 Script executed:

#!/bin/bash
# Description: 스케줄러에서 예외 처리가 구현되어 있는지 확인

# RefundScheduler 파일 찾기 및 예외 처리 로직 검색
fd -e java RefundScheduler --exec cat {} \; | grep -A 10 -B 5 "refundDonation"

Length of output: 327


스케줄러 레벨의 예외 처리를 더 자세히 검증하기 위해 RefundDonationService 구현을 확인하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# RefundDonationService의 refundDonation() 메소드 구현 확인
fd -e java RefundDonationService --exec cat {} \;

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 15

Length of output: 2399


🏁 Script executed:

#!/bin/bash
# Spring @Scheduled에 대한 에러 핸들링 설정 확인
rg -n "TaskScheduler\|SchedulingConfigurer\|@EnableScheduling" --type java -B 2 -A 5

Length of output: 0


추가로 프로젝트 레벨의 글로벌 에러 핸들링 설정을 확인하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# 글로벌 예외 처리 설정 확인 (AOP, Aspect, ErrorHandler 등)
rg -n "@Aspect\|@ControllerAdvice\|ErrorHandler\|ExceptionHandler" --type java -B 2 -A 10

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 10

Length of output: 6038


findDonationPort.findRefundableDonation() 호출에 예외 처리 추가 필요합니다.

검증 결과, RefundDonationService의 23-26번 줄에서 데이터베이스 조회 메소드를 호출할 때 예외 처리가 없습니다. DB 연결 실패, 타임아웃 등의 예외가 발생하면 스케줄러로 전파되어 실행 흐름이 중단됩니다. Spring @scheduled는 예외를 로깅하고 다음 스케줄에 계속 실행되지만, 애플리케이션 레벨의 명시적인 예외 처리 전략이 부재합니다.

다음과 같이 예외 처리를 추가하세요:

  • try-catch 블록으로 findRefundableDonation() 호출 감싸기
  • 또는 전체 refundDonation() 메소드에 예외 처리 추가
  • 예외 발생 시 적절한 로깅 및 알림 메커니즘 구현
🤖 Prompt for AI Agents
In src/main/java/com/donet/donet/donation/application/RefundDonationService.java
around lines 23-26, the call to findDonationPort.findRefundableDonation() lacks
exception handling and can propagate DB/connectivity/timeouts to the scheduler;
wrap the call (or the whole refundDonation() method) in a try-catch that catches
relevant exceptions (e.g., DataAccessException, RuntimeException), log the error
with context (including exception message and stacktrace) and return/exit
gracefully so the exception does not propagate to the scheduler, and optionally
trigger your existing alerting/notification mechanism when a failure occurs.


smartContractPort.refundDonations(refundTargetDonationIds);
}
Comment on lines 21 to 41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

스마트 컨트랙트 호출 결과를 처리하세요.

Line 26에서 smartContractPort.refundDonations()의 반환값을 무시하고 있습니다. 환불이 실패해도 감지할 수 없어 데이터 불일치가 발생할 수 있습니다.

다음을 추가하세요:

  • 반환값 확인 및 실패 시 로깅
  • 부분 실패 시나리오 처리
  • 메트릭 또는 알림을 통한 모니터링

다음과 같이 개선할 수 있습니다:

+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: 알림 또는 재시도 로직 추가 고려
+        }
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/donet/donet/donation/application/RefundDonationService.java
around lines 19 to 27, the result of smartContractPort.refundDonations(...) is
currently ignored which can hide failed refunds and cause data drift; update the
method to capture the return value, verify which donation IDs succeeded and
which failed, log failures with sufficient context, handle partial failures by
retrying or marking failed donations in the DB (or scheduling compensating
actions), and emit a metric/alert for non-zero failures so ops can monitor and
respond.

}
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
Expand Up @@ -18,4 +18,6 @@ public interface FindDonationPort {

List<JoinedDonation> findJoinedDonations(User user, int size);
List<RegisteredDonation> findRegisteredDonations(User user, int size);

List<Donation> findRefundableDonation();
}
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);
}
671 changes: 671 additions & 0 deletions src/main/java/com/donet/donet/global/smartContract/Campaign.java

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());
}

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

설정 값 검증 및 에러 처리 필요

PR 설명에서 언급한 대로 현재 환경 변수가 비어있어 런타임 에러가 발생할 수 있습니다. 빈 설정 또는 잘못된 형식의 값이 주입될 경우를 대비한 검증 로직을 추가하는 것을 권장합니다.

특히 다음 사항을 확인하세요:

  • privateKey가 유효한 형식인지 검증 (예: hex 문자열 길이 확인)
  • rpcUrl이 유효한 URL 형식인지 검증
  • campaignFactoryAddress가 유효한 이더리움 주소 형식인지 검증
  • 민감한 정보인 privateKey가 로그에 노출되지 않도록 주의

다음 스크립트로 애플리케이션이 시작될 때 설정 검증이 제대로 이루어지는지 확인하세요:


🏁 Script executed:

#!/bin/bash
# 설정 검증 로직이 있는지 확인
rg -n "privateKey|rpcUrl|campaignFactoryAddress" --type java -A 5 -B 5 -g '!**/test/**'

Length of output: 4046



---

🏁 Script executed:

```shell
#!/bin/bash
# View complete Web3jConfig.java file
cat src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java

Length of output: 1624


설정 값 검증 및 에러 처리 필수

현재 Web3jConfig.java의 설정 값들이 검증 없이 직접 사용되고 있습니다. 빈 또는 잘못된 형식의 설정이 주입될 경우 애플리케이션 시작 시점에 실패합니다.

다음 위치에서 검증 로직 추가가 필요합니다:

  • Line 39 (web3j() 메서드): rpcUrl을 HttpService에 전달하기 전에 유효한 URL 형식 확인
  • Line 44 (credentials() 메서드): privateKey를 Credentials.create에 전달하기 전에 유효한 hex 문자열 형증 (길이 130자 또는 128자)
  • Line 58 (campaignFactory() 메서드): campaignFactoryAddress를 CampaignFactory.load에 전달하기 전에 유효한 이더리움 주소 형식 확인 (0x로 시작하는 40자 hex)

추가로 @Value 필드에 @NotBlank, @NotEmpty 등의 검증 애노테이션을 추가하고, 민감한 정보인 privateKey가 에러 로그에 노출되지 않도록 주의하세요.

🤖 Prompt for AI Agents
In src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java (fields
around lines 21–35, methods referenced at ~39, 44, 58): add defensive validation
and safe error handling for injected properties — annotate rpcUrl, privateKey,
campaignFactoryAddress with @NotBlank/@NotEmpty, then in web3j() validate rpcUrl
is a well-formed HTTP(S) URL before passing to HttpService and throw a clear
IllegalArgumentException if invalid; in credentials() validate privateKey is a
hex string of 128 or 130 chars (allow optional 0x), do NOT log the key, and
throw a descriptive exception if invalid; in campaignFactory() validate
campaignFactoryAddress is a 0x-prefixed 40-byte hex address (or normalize by
adding 0x) before calling CampaignFactory.load and throw if invalid; ensure all
exceptions include non-sensitive details only and update method signatures/bean
creation to fail fast with clear messages.


@Bean
public Web3j web3j() {
return Web3j.build(new HttpService(rpcUrl));
}

@Bean
public Credentials credentials() {
return Credentials.create(privateKey);
}
Comment on lines +43 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Private key 보안 및 유효성 검증 필요

Credentials.create()는 유효하지 않은 private key 형식에 대해 예외를 발생시킵니다. Private key가 올바른 hex 형식인지 사전 검증하는 것을 권장합니다.

보안 주의사항:

  • Private key가 로그에 노출되지 않도록 주의하세요
  • 애플리케이션 설정 파일에 평문으로 저장하지 말고 환경 변수나 보안 볼트를 사용하세요
  • Private 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


더 정확한 검증을 위해 추가 정보가 필요합니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# Web3jConfig.java 전체 내용 확인
find . -name "Web3jConfig.java" -type f ! -path "*/test/*" -exec cat {} \;

Length of output: 1624


🏁 Script executed:

#!/bin/bash
# privateKey 필드 및 초기화 방식 확인
rg -n "privateKey" --type java -g '!**/test/**' -B5 -A5 | head -100

Length of output: 2167


🏁 Script executed:

#!/bin/bash
# 로깅 관련 설정 파일 확인
find . -name "logback*.xml" -o -name "log4j*.xml" -o -name "log4j*.properties" | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# 예외 처리 패턴 및 스택 트레이스 로깅 확인
rg -n "catch.*Exception|printStackTrace|logger.*Exception" --type java -g '!**/test/**' -C2 | head -50

Length of output: 5283


🏁 Script executed:

#!/bin/bash
# application.yml/properties 파일에서 privateKey 설정 방식 확인
find . -name "application*.yml" -o -name "application*.properties" | xargs cat 2>/dev/null

Length 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 -200

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Credentials 관련 예외 처리 확인
rg -n "Credentials\." --type java -g '!**/test/**' -B3 -A3

Length 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:

web3j Credentials.create() validation error handling private key

💡 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

  • Normalize: strip "0x" if present.
  • Ensure hex and length = 64; if shorter, pad left with '0' (leading zeros are valid private keys). If longer or non-hex, reject.
  • Wrap Credentials.create in try/catch and throw a clear error message.

Example (concise):

String normalizePrivateKey(String pk) {
  if (pk == null) throw new IllegalArgumentException("privateKey is null");
  pk = pk.startsWith("0x") ? pk.substring(2) : pk;
  if (!pk.matches("[0-9a-fA-F]+")) throw new IllegalArgumentException("privateKey must be hex");
  if (pk.length() > 64) throw new IllegalArgumentException("privateKey too long");
  if (pk.length() < 64) pk = String.format("%64s", pk).replace(' ', '0'); // pad left
  return pk;
}

try {
  String key = normalizePrivateKey(inputPrivateKey);
  Credentials creds = Credentials.create(key);
} catch (NumberFormatException | IllegalArgumentException e) {
  // handle invalid key: inform user / log / fail-safe
}

Notes:

  • Padding shorter keys to 64 hex chars is an accepted workaround (some key sources omit leading zeros). [2]
  • Always avoid logging or exposing private keys in error messages or logs.

Sources: web3j Credentials docs and community discussion about 63/64-char keys and padding. [1][2]

References
[1] web3j Credentials docs.
[2] Discussion (Web3Auth / StackOverflow echo) about 63-char private keys and padding.


Private key 유효성 검증 및 예외 처리 필수

Credentials.create(privateKey) 호출 시 유효하지 않은 형식에 대한 예외 처리와 사전 검증이 없습니다.

문제점:

  • 유효하지 않은 private key 형식(non-hex, 길이 부족 등)이 전달되면 NumberFormatException 또는 IllegalArgumentException 발생
  • 예외 처리 없이 bean 초기화 실패 시 스택 트레이스에 private key 값이 노출될 수 있음

권장사항:

  1. Private key 형식 검증 추가 (hex 여부, 길이 64자, "0x" prefix 제거)
  2. Credentials.create() 호출을 try-catch로 감싸고 명확한 에러 메시지 반환
  3. 에러 메시지에 private key 값을 포함하지 않기
  4. 환경 변수 설정 누락 시 애플리케이션 시작 시점에 실패하는 것이 바람직 (현재는 이미 적용 중)
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
In src/main/java/com/donet/donet/global/smartContract/Web3jConfig.java around
lines 43-45, the credentials() bean directly calls
Credentials.create(privateKey) without validating the key or handling
exceptions; add a normalize step that checks for null/empty, strips a leading
"0x", verifies the string is hexadecimal and exactly 64 characters, then call
Credentials.create with the normalized key inside a try-catch that wraps
IllegalArgumentException/NumberFormatException into a RuntimeException with a
clear message that does NOT include the private key value; this ensures
invalid/missing env config fails fast during startup and prevents leaking the
private key in stack traces.


@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);
}
}
9 changes: 9 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,12 @@ management:
show-details: "always" # 상세 정보 포함
server:
port: 8080

web3:
rpcUrl: ${RPC_URL}
privateKey: ${CONTRACT_PRIVATE_KEY}
gasPrice: ${GAS_PRICE}
gasLimit: ${GAS_LIMIT}

contracts:
campaignFactoryAddress: ${CONTRACT_ADDRESS}
Loading