Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.stablecoin.payments.custody.infrastructure.persistence;

import com.stablecoin.payments.custody.domain.model.ChainTransfer;
import com.stablecoin.payments.custody.domain.model.TransferStatus;
import com.stablecoin.payments.custody.domain.model.TransferType;
import com.stablecoin.payments.custody.domain.port.ChainTransferRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.entity.ChainTransferJpaRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.mapper.ChainTransferEntityUpdater;
import com.stablecoin.payments.custody.infrastructure.persistence.mapper.ChainTransferPersistenceMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Slf4j
@Repository
@RequiredArgsConstructor
public class ChainTransferPersistenceAdapter implements ChainTransferRepository {

private final ChainTransferJpaRepository jpa;
private final ChainTransferPersistenceMapper mapper;
private final ChainTransferEntityUpdater updater;

@Override
public ChainTransfer save(ChainTransfer transfer) {
var existing = jpa.findById(transfer.transferId());
if (existing.isPresent()) {
updater.updateEntity(existing.get(), transfer);
return mapper.toDomain(jpa.save(existing.get()));
}
return mapper.toDomain(jpa.save(mapper.toEntity(transfer)));
}
Comment on lines +27 to +35

Copy link
Copy Markdown

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

🏁 Script executed:

#!/bin/bash
# Check if callers of save() are wrapped in `@Transactional`
rg -n 'ChainTransferRepository' --type java -A 3 | head -60

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2467


🏁 Script executed:

#!/bin/bash
# Read full ChainTransferPersistenceAdapter to see all annotations
cat -n blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/ChainTransferPersistenceAdapter.java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2523


🏁 Script executed:

#!/bin/bash
# Find all usages of ChainTransferRepository.save() and callers
rg -n '\.save\(' blockchain-custody/blockchain-custody/src/main/java --type java -B 2 -A 2 | grep -E '(save|Transactional|interface|class)' | head -80

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2275


🏁 Script executed:

#!/bin/bash
# Check use case files that might call ChainTransferRepository
fd -e 'java' -path '*use*case*' blockchain-custody/blockchain-custody/src/main/java | head -20

Repository: Puneethkumarck/stablebridge-platform

Length of output: 248


🏁 Script executed:

#!/bin/bash
# Search for files that import or use ChainTransferRepository
rg -l 'ChainTransferRepository' blockchain-custody/blockchain-custody/src/main/java --type java | grep -v PersistenceAdapter

Repository: Puneethkumarck/stablebridge-platform

Length of output: 204


🏁 Script executed:

#!/bin/bash
# Search for use case or service files that might use ChainTransferRepository
find blockchain-custody/blockchain-custody/src/main/java -name '*UseCase.java' -o -name '*Service.java' | head -20

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Look for `@Transactional` annotations on methods that use repositories
rg -n '@Transactional' blockchain-custody/blockchain-custody/src/main/java --type java | head -30

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Broader search for `@Transactional` across all modules
find blockchain-custody -name '*.java' -exec grep -l '@Transactional' {} \; | head -20

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Search for methods calling repository.save()
rg -n 'repository\.save\(' blockchain-custody --type java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Look for any Spring service or component classes
find blockchain-custody -name '*.java' -exec grep -l '@Service\|@Component' {} \; | head -15

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Check for application.yml/properties to understand Spring config
find blockchain-custody -name 'application*.properties' -o -name 'application*.yml'

Repository: Puneethkumarck/stablebridge-platform

Length of output: 254


🏁 Script executed:

#!/bin/bash
# Check application.yml for transaction settings
cat blockchain-custody/blockchain-custody/src/main/resources/application.yml

Repository: Puneethkumarck/stablebridge-platform

Length of output: 3143


🏁 Script executed:

#!/bin/bash
# Look for Spring Data JPA repository base class or implicit `@Transactional`
rg -n 'extends JpaRepository\|extends CrudRepository' blockchain-custody --type java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Check if there are integration tests that show how repositories are used
find blockchain-custody -path '*test*' -name '*.java' | head -10

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1289


🏁 Script executed:

#!/bin/bash
# Search more broadly for any method that injects ChainTransferRepository
rg -n 'ChainTransferRepository' blockchain-custody --type java -A 5

Repository: Puneethkumarck/stablebridge-platform

Length of output: 3511


🏁 Script executed:

#!/bin/bash
# Check ChainTransferJpaRepository to understand if it has any transaction handling
find blockchain-custody -name 'ChainTransferJpaRepository.java'

Repository: Puneethkumarck/stablebridge-platform

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Look for any Kafka event handler or listener that might call the repository
rg -n '@KafkaListener\|@EventListener' blockchain-custody --type java -A 5

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Check if there are any outbox or event publishing patterns that would use the repository
rg -n 'Outbox\|Event' blockchain-custody/blockchain-custody/src/main/java --type java -l | head -15

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Read ChainTransferJpaRepository to see if it extends Spring Data interfaces
cat blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferJpaRepository.java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 693


🏁 Script executed:

#!/bin/bash
# Search for all files that mention 'save' and 'ChainTransfer' to find callers
rg -n 'ChainTransfer' blockchain-custody/blockchain-custody/src/main/java --type java | grep -i 'save\|create\|update' | head -20

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1833


🏁 Script executed:

#!/bin/bash
# Check if there are domain use cases that would use the repository
find blockchain-custody -type f -name '*.java' -path '*/domain/*' | xargs grep -l 'ChainTransferRepository' 2>/dev/null

Repository: Puneethkumarck/stablebridge-platform

Length of output: 204


🏁 Script executed:

#!/bin/bash
# Broader search for any file containing 'save' method calls on repositories
rg -n '\.save\(' blockchain-custody/blockchain-custody/src --type java -B 3 | grep -E '(class|interface|public|private|save\()' | head -40

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2275


🏁 Script executed:

#!/bin/bash
# Final check: look at WalletPersistenceAdapter to see if it has `@Transactional` since it uses same pattern
cat blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/WalletPersistenceAdapter.java | head -50

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1971


Add @Transactional to save() for atomicity.

The find-then-update-then-save pattern (lines 29–34) is not atomic without an explicit transaction boundary. Each operation runs independently, creating a TOCTOU window for concurrent saves. Spring Data's JpaRepository.save() inherits the caller's transactional context—it does not provide its own.

While no callers currently exist, this pattern appears across adapters (e.g., WalletPersistenceAdapter), pushing transaction responsibility to clients. Simpler to make it safe at the adapter layer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/ChainTransferPersistenceAdapter.java`
around lines 27 - 35, The save method in ChainTransferPersistenceAdapter should
be made transactional to prevent TOCTOU races: annotate the save(ChainTransfer
transfer) method with Spring's `@Transactional` so the sequence of
jpa.findById(...), updater.updateEntity(...), and jpa.save(...) executes in one
transaction; ensure the annotation is applied to the
ChainTransferPersistenceAdapter.save method (or class) and import
org.springframework.transaction.annotation.Transactional so
updater.updateEntity(existing.get(), transfer) and mapper/jpa save calls run
atomically.


@Override
public Optional<ChainTransfer> findById(UUID transferId) {
return jpa.findById(transferId).map(mapper::toDomain);
}

@Override
public Optional<ChainTransfer> findByPaymentIdAndType(UUID paymentId, TransferType type) {
return jpa.findByPaymentIdAndTransferType(paymentId, type).map(mapper::toDomain);
}

@Override
public List<ChainTransfer> findByStatus(TransferStatus status) {
return jpa.findByStatus(status).stream()
.map(mapper::toDomain)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.stablecoin.payments.custody.infrastructure.persistence;

import com.stablecoin.payments.custody.domain.model.TransferLifecycleEvent;
import com.stablecoin.payments.custody.domain.port.TransferLifecycleEventRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.entity.TransferLifecycleEventJpaRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.mapper.TransferLifecycleEventPersistenceMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.UUID;

@Slf4j
@Repository
@RequiredArgsConstructor
public class TransferLifecycleEventPersistenceAdapter implements TransferLifecycleEventRepository {

private final TransferLifecycleEventJpaRepository jpa;
private final TransferLifecycleEventPersistenceMapper mapper;

@Override
public TransferLifecycleEvent save(TransferLifecycleEvent event) {
return mapper.toDomain(jpa.save(mapper.toEntity(event)));
}

@Override
public List<TransferLifecycleEvent> findByTransferId(UUID transferId) {
return jpa.findByTransferId(transferId).stream()
.map(mapper::toDomain)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.stablecoin.payments.custody.infrastructure.persistence;

import com.stablecoin.payments.custody.domain.model.TransferParticipant;
import com.stablecoin.payments.custody.domain.port.TransferParticipantRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.entity.TransferParticipantJpaRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.mapper.TransferParticipantPersistenceMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.UUID;

@Slf4j

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unused @Slf4j annotation.

Logger is declared but never used. Either add logging for persistence operations or remove the annotation.

♻️ Option A: Remove unused annotation
-@Slf4j
 `@Repository`
 `@RequiredArgsConstructor`
♻️ Option B: Add debug logging
     `@Override`
     public TransferParticipant save(TransferParticipant participant) {
-        return mapper.toDomain(jpa.save(mapper.toEntity(participant)));
+        log.debug("Saving transfer participant: {}", participant.participantId());
+        var saved = mapper.toDomain(jpa.save(mapper.toEntity(participant)));
+        log.debug("Saved transfer participant: {}", saved.participantId());
+        return saved;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Slf4j
`@Repository`
`@RequiredArgsConstructor`
Suggested change
@Slf4j
`@Override`
public TransferParticipant save(TransferParticipant participant) {
log.debug("Saving transfer participant: {}", participant.participantId());
var saved = mapper.toDomain(jpa.save(mapper.toEntity(participant)));
log.debug("Saved transfer participant: {}", saved.participantId());
return saved;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/TransferParticipantPersistenceAdapter.java`
at line 14, The `@Slf4j` annotation on TransferParticipantPersistenceAdapter is
unused; either remove the annotation or add relevant log statements for
persistence operations (e.g., in methods save, findById, delete or constructor)
so the generated 'log' field is actually used; update
TransferParticipantPersistenceAdapter to remove `@Slf4j` if you don't want
logging, or insert debug/info/error logs in key methods of that class (use
method names like save, findById, delete or the class constructor) to record
operation start, success, and failure.

@Repository
@RequiredArgsConstructor
public class TransferParticipantPersistenceAdapter implements TransferParticipantRepository {

private final TransferParticipantJpaRepository jpa;
private final TransferParticipantPersistenceMapper mapper;

@Override
public TransferParticipant save(TransferParticipant participant) {
return mapper.toDomain(jpa.save(mapper.toEntity(participant)));
}

@Override
public List<TransferParticipant> findByTransferId(UUID transferId) {
return jpa.findByTransferId(transferId).stream()
.map(mapper::toDomain)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.stablecoin.payments.custody.infrastructure.persistence;

import com.stablecoin.payments.custody.domain.model.StablecoinTicker;
import com.stablecoin.payments.custody.domain.model.WalletBalance;
import com.stablecoin.payments.custody.domain.port.WalletBalanceRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.entity.WalletBalanceJpaRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.mapper.WalletBalanceEntityUpdater;
import com.stablecoin.payments.custody.infrastructure.persistence.mapper.WalletBalancePersistenceMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Slf4j
@Repository
@RequiredArgsConstructor
public class WalletBalancePersistenceAdapter implements WalletBalanceRepository {

private final WalletBalanceJpaRepository jpa;
private final WalletBalancePersistenceMapper mapper;
private final WalletBalanceEntityUpdater updater;

@Override
public WalletBalance save(WalletBalance balance) {
var existing = jpa.findById(balance.balanceId());
if (existing.isPresent()) {
updater.updateEntity(existing.get(), balance);
return mapper.toDomain(jpa.save(existing.get()));
}
return mapper.toDomain(jpa.save(mapper.toEntity(balance)));
}

@Override
public Optional<WalletBalance> findByWalletIdAndStablecoin(UUID walletId, StablecoinTicker stablecoin) {
return jpa.findByWalletIdAndStablecoin(walletId, stablecoin.ticker())
.map(mapper::toDomain);
}

@Override
public Optional<WalletBalance> findByWalletIdAndStablecoinForUpdate(UUID walletId, StablecoinTicker stablecoin) {
return jpa.findByWalletIdAndStablecoinForUpdate(walletId, stablecoin.ticker())
.map(mapper::toDomain);
}

@Override
public List<WalletBalance> findByWalletId(UUID walletId) {
return jpa.findByWalletId(walletId).stream()
.map(mapper::toDomain)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.stablecoin.payments.custody.infrastructure.persistence;

import com.stablecoin.payments.custody.domain.model.ChainId;
import com.stablecoin.payments.custody.domain.model.Wallet;
import com.stablecoin.payments.custody.domain.model.WalletPurpose;
import com.stablecoin.payments.custody.domain.port.WalletRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.entity.WalletJpaRepository;
import com.stablecoin.payments.custody.infrastructure.persistence.mapper.WalletEntityUpdater;
import com.stablecoin.payments.custody.infrastructure.persistence.mapper.WalletPersistenceMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Slf4j

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove unused @Slf4j or add logging.

The annotation is present but no log.* calls exist. Either add debug/trace logging for persistence operations or remove the annotation to reduce noise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/WalletPersistenceAdapter.java`
at line 18, The `@Slf4j` annotation on the WalletPersistenceAdapter class is
unused; either remove it or add logging statements for persistence operations.
To fix, open the WalletPersistenceAdapter class and either delete the `@Slf4j`
import/annotation or add meaningful log calls (e.g., log.debug/info/error)
inside methods like saveWallet, findWalletById, and deleteWallet to record
entry/exit, key parameters, and error conditions; keep logs concise and
sensitive-data-free and ensure imports are adjusted to match the change.

@Repository
@RequiredArgsConstructor
public class WalletPersistenceAdapter implements WalletRepository {

private final WalletJpaRepository jpa;
private final WalletPersistenceMapper mapper;
private final WalletEntityUpdater updater;

@Override
public Wallet save(Wallet wallet) {
var existing = jpa.findById(wallet.walletId());
if (existing.isPresent()) {
updater.updateEntity(existing.get(), wallet);
return mapper.toDomain(jpa.save(existing.get()));
}
return mapper.toDomain(jpa.save(mapper.toEntity(wallet)));
}
Comment on lines +27 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add @Transactional to ensure atomic find-then-update semantics.

The save() method performs a read followed by a write. Without an explicit transaction boundary, concurrent calls for the same walletId may race, leading to constraint violations or lost updates. Wrapping in @Transactional ensures the entire operation is atomic.

🔒 Proposed fix
+import org.springframework.transaction.annotation.Transactional;
 ...
 
+    `@Transactional`
     `@Override`
     public Wallet save(Wallet wallet) {
         var existing = jpa.findById(wallet.walletId());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/WalletPersistenceAdapter.java`
around lines 27 - 35, The save() method in WalletPersistenceAdapter does a
read-then-write (jpa.findById(...) then jpa.save(...)) which can race; annotate
the WalletPersistenceAdapter.save(Wallet) method with `@Transactional` so the
find-and-update path (including updater.updateEntity(existing.get(), wallet) and
subsequent jpa.save(existing.get())) executes in a single transaction, ensuring
atomicity and preventing lost updates or constraint violations; import and use
the platform-appropriate transactional annotation (e.g.,
org.springframework.transaction.annotation.Transactional) on the save method or
the class as appropriate.


@Override
public Optional<Wallet> findById(UUID walletId) {
return jpa.findById(walletId).map(mapper::toDomain);
}

@Override
public List<Wallet> findByChainIdAndPurpose(ChainId chainId, WalletPurpose purpose) {
return jpa.findByChainIdAndPurposeAndActiveTrue(chainId.value(), purpose).stream()
.map(mapper::toDomain)
.toList();
}

@Override
public Optional<Wallet> findByAddress(String address) {
return jpa.findByAddress(address).map(mapper::toDomain);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.stablecoin.payments.custody.infrastructure.persistence.entity;

import com.stablecoin.payments.custody.domain.model.TransferStatus;
import com.stablecoin.payments.custody.domain.model.TransferType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.UUID;

@Entity
@Table(name = "chain_transfers")
Comment on lines +21 to +22

Copy link
Copy Markdown

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

🏁 Script executed:

#!/bin/bash
# Check for existing indexes on chain_transfers table in migration files
rg -n 'CREATE.*INDEX.*chain_transfers' --type sql

Repository: Puneethkumarck/stablebridge-platform

Length of output: 494


🏁 Script executed:

# Check the exact definitions of the indexes in the migration file
rg -A 2 'CREATE.*INDEX.*chain_transfers' blockchain-custody/blockchain-custody/src/main/resources/db/migration/V1__initial_schema.sql

Repository: Puneethkumarck/stablebridge-platform

Length of output: 426


🏁 Script executed:

# Find and examine ChainTransferJpaRepository to see actual query methods
fd -n 'ChainTransferJpaRepository' blockchain-custody/blockchain-custody/src

Repository: Puneethkumarck/stablebridge-platform

Length of output: 310


🏁 Script executed:

# Find ChainTransferJpaRepository with correct syntax
fd 'ChainTransferJpaRepository' blockchain-custody/blockchain-custody/src

Repository: Puneethkumarck/stablebridge-platform

Length of output: 229


🏁 Script executed:

# Also search for any query methods that use transfer_type and payment_id together
rg -A 5 'findByPaymentId' blockchain-custody/blockchain-custody/src --type java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2867


🏁 Script executed:

# Verify transfer_type field exists in ChainTransferEntity
rg -A 2 'transfer_type|transferType' blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferEntity.java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 209


Add composite index on (payment_id, transfer_type) for query optimization.

The findByPaymentIdAndTransferType query will table-scan or use index on payment_id alone, then filter by transfer_type in-memory. Add composite index (payment_id, transfer_type) in the migration or via @Index on @Table.

Note: status index already exists in migration, so no action needed there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferEntity.java`
around lines 21 - 22, Add a composite DB index for queries filtering by
paymentId and transferType: update the ChainTransferEntity class annotation (the
`@Table` on class ChainTransferEntity) to include an `@Index` for the column pair
(payment_id, transfer_type) or alternatively add a DB migration that creates the
composite index; ensure the index name is unique and matches the entity columns
used by the findByPaymentIdAndTransferType query so the JPA repository uses the
composite index instead of scanning/filtering in memory.

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChainTransferEntity {

@Id
@Column(name = "transfer_id", updatable = false)
private UUID transferId;

@Column(name = "payment_id")
private UUID paymentId;

@Column(name = "correlation_id")
private UUID correlationId;

@Enumerated(EnumType.STRING)
@Column(name = "transfer_type", length = 20)
private TransferType transferType;

@Column(name = "parent_transfer_id")
private UUID parentTransferId;

@Column(name = "chain_id", length = 20)
private String chainId;

@Column(name = "stablecoin", length = 20)
private String stablecoin;

@Column(name = "amount", precision = 30, scale = 8)
private BigDecimal amount;

@Column(name = "from_wallet_id")
private UUID fromWalletId;

@Column(name = "to_address", length = 128)
private String toAddress;

@Column(name = "from_address", length = 128)
private String fromAddress;

@Column(name = "nonce")
private Long nonce;

@Column(name = "tx_hash", length = 128)
private String txHash;

@Enumerated(EnumType.STRING)
@Column(name = "status", length = 30)
private TransferStatus status;

@Column(name = "block_number")
private Long blockNumber;

@Column(name = "block_confirmed_at")
private Instant blockConfirmedAt;

@Column(name = "confirmations")
private Integer confirmations;

@Column(name = "gas_used", precision = 20, scale = 0)
private BigDecimal gasUsed;

@Column(name = "gas_price_gwei", precision = 20, scale = 9)
private BigDecimal gasPriceGwei;

@Column(name = "attempt_count")
private int attemptCount;

@Column(name = "failure_reason")
private String failureReason;

@Column(name = "error_code", length = 100)
private String errorCode;

@Column(name = "created_at", updatable = false)
private Instant createdAt;

@Column(name = "updated_at")
private Instant updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stablecoin.payments.custody.infrastructure.persistence.entity;

import com.stablecoin.payments.custody.domain.model.TransferStatus;
import com.stablecoin.payments.custody.domain.model.TransferType;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

public interface ChainTransferJpaRepository extends JpaRepository<ChainTransferEntity, UUID> {

Optional<ChainTransferEntity> findByPaymentIdAndTransferType(UUID paymentId, TransferType type);

List<ChainTransferEntity> findByStatus(TransferStatus status);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider pagination for findByStatus.

This returns an unbounded List which could be problematic if many transfers accumulate in a given status (e.g., PENDING or FAILED during outages). If this is used for batch processing, consider Page<ChainTransferEntity> findByStatus(TransferStatus status, Pageable pageable) or at minimum add LIMIT via @Query.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferJpaRepository.java`
at line 15, The current repository method findByStatus in
ChainTransferJpaRepository returns an unbounded List which can OOM for
high-volume statuses; update the repository API to support pagination by
changing the signature to return Page<ChainTransferEntity> and accept a Pageable
(e.g., Page<ChainTransferEntity> findByStatus(TransferStatus status, Pageable
pageable)), or alternatively add a `@Query` with a LIMIT parameter for bounded
fetches; ensure callers that perform batch processing are updated to pass a
Pageable (or page/size) and handle Page metadata (totalPages/hasNext)
accordingly.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.stablecoin.payments.custody.infrastructure.persistence.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.Instant;
import java.util.UUID;

@Entity
@Table(name = "transfer_lifecycle_events")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TransferLifecycleEventEntity {

@Id
@Column(name = "event_id", updatable = false)
private UUID eventId;

@Column(name = "transfer_id")
private UUID transferId;
Comment on lines +29 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding an index on transfer_id.

The findByTransferId query in the repository will scan without an index. For an append-only event log, this column will be queried frequently and the table will grow unbounded.

Add a table index in your migration script:

CREATE INDEX idx_transfer_lifecycle_events_transfer_id 
ON transfer_lifecycle_events(transfer_id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferLifecycleEventEntity.java`
around lines 29 - 30, Add a DB index on transfer_id to avoid full-table scans
for the repository method (e.g., findByTransferId). Update your migration
scripts to create an index like idx_transfer_lifecycle_events_transfer_id on the
transfer_lifecycle_events(transfer_id); then run/apply the migration and ensure
the JPA entity TransferLifecycleEventEntity (field transferId) remains
unchanged; also verify any existing repository methods referencing transferId
still work after migration.


@Column(name = "state", length = 50)
private String state;

@Column(name = "participant_type", length = 10)
private String participantType;
Comment on lines +32 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using enums for state and participantType.

TransferParticipantEntity uses @Enumerated(EnumType.STRING) for participantType. Using String here creates inconsistency and loses compile-time type safety.

If the domain types are enums, prefer:

`@Enumerated`(EnumType.STRING)
`@Column`(name = "state", length = 50)
private TransferState state;

`@Enumerated`(EnumType.STRING)
`@Column`(name = "participant_type", length = 10)
private ParticipantType participantType;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferLifecycleEventEntity.java`
around lines 32 - 36, In TransferLifecycleEventEntity replace the String fields
state and participantType with the appropriate enum types (e.g., TransferState
and ParticipantType), annotate both with `@Enumerated`(EnumType.STRING) and keep
the existing `@Column`(...) annotations; update the field declarations, imports,
and any getters/setters or usages in methods/constructors to use the enum types
so mapping aligns with TransferParticipantEntity and preserves compile-time
safety.


@Column(name = "address", length = 128)
private String address;

@Column(name = "occurred_at")
private Instant occurredAt;
}
Loading