diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/ChainTransferPersistenceAdapter.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/ChainTransferPersistenceAdapter.java new file mode 100644 index 00000000..b0022962 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/ChainTransferPersistenceAdapter.java @@ -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))); + } + + @Override + public Optional findById(UUID transferId) { + return jpa.findById(transferId).map(mapper::toDomain); + } + + @Override + public Optional findByPaymentIdAndType(UUID paymentId, TransferType type) { + return jpa.findByPaymentIdAndTransferType(paymentId, type).map(mapper::toDomain); + } + + @Override + public List findByStatus(TransferStatus status) { + return jpa.findByStatus(status).stream() + .map(mapper::toDomain) + .toList(); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/TransferLifecycleEventPersistenceAdapter.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/TransferLifecycleEventPersistenceAdapter.java new file mode 100644 index 00000000..25bed48b --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/TransferLifecycleEventPersistenceAdapter.java @@ -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 findByTransferId(UUID transferId) { + return jpa.findByTransferId(transferId).stream() + .map(mapper::toDomain) + .toList(); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/TransferParticipantPersistenceAdapter.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/TransferParticipantPersistenceAdapter.java new file mode 100644 index 00000000..88713017 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/TransferParticipantPersistenceAdapter.java @@ -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 +@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 findByTransferId(UUID transferId) { + return jpa.findByTransferId(transferId).stream() + .map(mapper::toDomain) + .toList(); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/WalletBalancePersistenceAdapter.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/WalletBalancePersistenceAdapter.java new file mode 100644 index 00000000..bf054309 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/WalletBalancePersistenceAdapter.java @@ -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 findByWalletIdAndStablecoin(UUID walletId, StablecoinTicker stablecoin) { + return jpa.findByWalletIdAndStablecoin(walletId, stablecoin.ticker()) + .map(mapper::toDomain); + } + + @Override + public Optional findByWalletIdAndStablecoinForUpdate(UUID walletId, StablecoinTicker stablecoin) { + return jpa.findByWalletIdAndStablecoinForUpdate(walletId, stablecoin.ticker()) + .map(mapper::toDomain); + } + + @Override + public List findByWalletId(UUID walletId) { + return jpa.findByWalletId(walletId).stream() + .map(mapper::toDomain) + .toList(); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/WalletPersistenceAdapter.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/WalletPersistenceAdapter.java new file mode 100644 index 00000000..f90495de --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/WalletPersistenceAdapter.java @@ -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 +@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))); + } + + @Override + public Optional findById(UUID walletId) { + return jpa.findById(walletId).map(mapper::toDomain); + } + + @Override + public List findByChainIdAndPurpose(ChainId chainId, WalletPurpose purpose) { + return jpa.findByChainIdAndPurposeAndActiveTrue(chainId.value(), purpose).stream() + .map(mapper::toDomain) + .toList(); + } + + @Override + public Optional findByAddress(String address) { + return jpa.findByAddress(address).map(mapper::toDomain); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferEntity.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferEntity.java new file mode 100644 index 00000000..e0c8da8d --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferEntity.java @@ -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") +@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; +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferJpaRepository.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferJpaRepository.java new file mode 100644 index 00000000..9f93d429 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/ChainTransferJpaRepository.java @@ -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 { + + Optional findByPaymentIdAndTransferType(UUID paymentId, TransferType type); + + List findByStatus(TransferStatus status); +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferLifecycleEventEntity.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferLifecycleEventEntity.java new file mode 100644 index 00000000..bd187cfb --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferLifecycleEventEntity.java @@ -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; + + @Column(name = "state", length = 50) + private String state; + + @Column(name = "participant_type", length = 10) + private String participantType; + + @Column(name = "address", length = 128) + private String address; + + @Column(name = "occurred_at") + private Instant occurredAt; +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferLifecycleEventJpaRepository.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferLifecycleEventJpaRepository.java new file mode 100644 index 00000000..4a947961 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferLifecycleEventJpaRepository.java @@ -0,0 +1,11 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.entity; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +public interface TransferLifecycleEventJpaRepository extends JpaRepository { + + List findByTransferId(UUID transferId); +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferParticipantEntity.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferParticipantEntity.java new file mode 100644 index 00000000..39c5f4c6 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferParticipantEntity.java @@ -0,0 +1,50 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.entity; + +import com.stablecoin.payments.custody.domain.model.ParticipantType; +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.util.UUID; + +@Entity +@Table(name = "transfer_participants") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransferParticipantEntity { + + @Id + @Column(name = "participant_id", updatable = false) + private UUID participantId; + + @Column(name = "transfer_id") + private UUID transferId; + + @Enumerated(EnumType.STRING) + @Column(name = "participant_type", length = 10) + private ParticipantType participantType; + + @Column(name = "address", length = 128) + private String address; + + @Column(name = "wallet_id") + private UUID walletId; + + @Column(name = "amount", precision = 30, scale = 8) + private BigDecimal amount; + + @Column(name = "asset_code", length = 20) + private String assetCode; +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferParticipantJpaRepository.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferParticipantJpaRepository.java new file mode 100644 index 00000000..f6f6304e --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/TransferParticipantJpaRepository.java @@ -0,0 +1,11 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.entity; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +public interface TransferParticipantJpaRepository extends JpaRepository { + + List findByTransferId(UUID transferId); +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletBalanceEntity.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletBalanceEntity.java new file mode 100644 index 00000000..65080361 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletBalanceEntity.java @@ -0,0 +1,58 @@ +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 jakarta.persistence.Version; +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 = "wallet_balances") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WalletBalanceEntity { + + @Id + @Column(name = "balance_id", updatable = false) + private UUID balanceId; + + @Column(name = "wallet_id") + private UUID walletId; + + @Column(name = "chain_id", length = 20) + private String chainId; + + @Column(name = "stablecoin", length = 20) + private String stablecoin; + + @Column(name = "available_balance", precision = 30, scale = 8) + private BigDecimal availableBalance; + + @Column(name = "reserved_balance", precision = 30, scale = 8) + private BigDecimal reservedBalance; + + @Column(name = "blockchain_balance", precision = 30, scale = 8) + private BigDecimal blockchainBalance; + + @Column(name = "last_indexed_block") + private long lastIndexedBlock; + + @Version + @Column(name = "version") + private Long version; + + @Column(name = "updated_at") + private Instant updatedAt; +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletBalanceJpaRepository.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletBalanceJpaRepository.java new file mode 100644 index 00000000..bf1e3bf7 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletBalanceJpaRepository.java @@ -0,0 +1,24 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.entity; + +import jakarta.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface WalletBalanceJpaRepository extends JpaRepository { + + Optional findByWalletIdAndStablecoin(UUID walletId, String stablecoin); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT wb FROM WalletBalanceEntity wb WHERE wb.walletId = :walletId AND wb.stablecoin = :stablecoin") + Optional findByWalletIdAndStablecoinForUpdate( + @Param("walletId") UUID walletId, + @Param("stablecoin") String stablecoin); + + List findByWalletId(UUID walletId); +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletEntity.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletEntity.java new file mode 100644 index 00000000..ba6a6bf4 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletEntity.java @@ -0,0 +1,67 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.entity; + +import com.stablecoin.payments.custody.domain.model.WalletPurpose; +import com.stablecoin.payments.custody.domain.model.WalletTier; +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.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "wallets") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WalletEntity { + + @Id + @Column(name = "wallet_id", updatable = false) + private UUID walletId; + + @Column(name = "chain_id", length = 20) + private String chainId; + + @Column(name = "address", length = 128) + private String address; + + @Column(name = "address_checksum", length = 128) + private String addressChecksum; + + @Enumerated(EnumType.STRING) + @Column(name = "tier", length = 10) + private WalletTier tier; + + @Enumerated(EnumType.STRING) + @Column(name = "purpose", length = 20) + private WalletPurpose purpose; + + @Column(name = "custodian", length = 50) + private String custodian; + + @Column(name = "vault_account_id", length = 200) + private String vaultAccountId; + + @Column(name = "stablecoin", length = 20) + private String stablecoin; + + @Column(name = "is_active") + private boolean active; + + @Column(name = "created_at", updatable = false) + private Instant createdAt; + + @Column(name = "deactivated_at") + private Instant deactivatedAt; +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletJpaRepository.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletJpaRepository.java new file mode 100644 index 00000000..e6b7a36e --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/entity/WalletJpaRepository.java @@ -0,0 +1,15 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.entity; + +import com.stablecoin.payments.custody.domain.model.WalletPurpose; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface WalletJpaRepository extends JpaRepository { + + List findByChainIdAndPurposeAndActiveTrue(String chainId, WalletPurpose purpose); + + Optional findByAddress(String address); +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/ChainTransferEntityUpdater.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/ChainTransferEntityUpdater.java new file mode 100644 index 00000000..65992afc --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/ChainTransferEntityUpdater.java @@ -0,0 +1,38 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.mapper; + +import com.stablecoin.payments.custody.domain.model.ChainTransfer; +import com.stablecoin.payments.custody.infrastructure.persistence.entity.ChainTransferEntity; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +@Mapper +public interface ChainTransferEntityUpdater { + + default void updateEntity(@MappingTarget ChainTransferEntity entity, ChainTransfer transfer) { + if (transfer == null) { + return; + } + entity.setPaymentId(transfer.paymentId()); + entity.setCorrelationId(transfer.correlationId()); + entity.setTransferType(transfer.transferType()); + entity.setParentTransferId(transfer.parentTransferId()); + entity.setChainId(transfer.chainId() != null ? transfer.chainId().value() : null); + entity.setStablecoin(transfer.stablecoin() != null ? transfer.stablecoin().ticker() : null); + entity.setAmount(transfer.amount()); + entity.setFromWalletId(transfer.fromWalletId()); + entity.setToAddress(transfer.toWalletAddress()); + entity.setFromAddress(transfer.fromAddress()); + entity.setNonce(transfer.nonce()); + entity.setTxHash(transfer.txHash()); + entity.setStatus(transfer.status()); + entity.setBlockNumber(transfer.blockNumber()); + entity.setBlockConfirmedAt(transfer.blockConfirmedAt()); + entity.setConfirmations(transfer.confirmations()); + entity.setGasUsed(transfer.gasUsed()); + entity.setGasPriceGwei(transfer.gasPriceGwei()); + entity.setAttemptCount(transfer.attemptCount()); + entity.setFailureReason(transfer.failureReason()); + entity.setErrorCode(transfer.errorCode()); + entity.setUpdatedAt(transfer.updatedAt()); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/ChainTransferPersistenceMapper.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/ChainTransferPersistenceMapper.java new file mode 100644 index 00000000..3e1f6af7 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/ChainTransferPersistenceMapper.java @@ -0,0 +1,86 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.mapper; + +import com.stablecoin.payments.custody.domain.model.ChainId; +import com.stablecoin.payments.custody.domain.model.ChainTransfer; +import com.stablecoin.payments.custody.domain.model.StablecoinTicker; +import com.stablecoin.payments.custody.infrastructure.persistence.entity.ChainTransferEntity; +import org.mapstruct.Mapper; + +@Mapper +public interface ChainTransferPersistenceMapper { + + default ChainTransferEntity toEntity(ChainTransfer transfer) { + if (transfer == null) { + return null; + } + return ChainTransferEntity.builder() + .transferId(transfer.transferId()) + .paymentId(transfer.paymentId()) + .correlationId(transfer.correlationId()) + .transferType(transfer.transferType()) + .parentTransferId(transfer.parentTransferId()) + .chainId(transfer.chainId() != null ? transfer.chainId().value() : null) + .stablecoin(transfer.stablecoin() != null ? transfer.stablecoin().ticker() : null) + .amount(transfer.amount()) + .fromWalletId(transfer.fromWalletId()) + .toAddress(transfer.toWalletAddress()) + .fromAddress(transfer.fromAddress()) + .nonce(transfer.nonce()) + .txHash(transfer.txHash()) + .status(transfer.status()) + .blockNumber(transfer.blockNumber()) + .blockConfirmedAt(transfer.blockConfirmedAt()) + .confirmations(transfer.confirmations()) + .gasUsed(transfer.gasUsed()) + .gasPriceGwei(transfer.gasPriceGwei()) + .attemptCount(transfer.attemptCount()) + .failureReason(transfer.failureReason()) + .errorCode(transfer.errorCode()) + .createdAt(transfer.createdAt()) + .updatedAt(transfer.updatedAt()) + .build(); + } + + default ChainTransfer toDomain(ChainTransferEntity entity) { + if (entity == null) { + return null; + } + + ChainId chainId = null; + if (entity.getChainId() != null) { + chainId = new ChainId(entity.getChainId()); + } + + StablecoinTicker stablecoin = null; + if (entity.getStablecoin() != null) { + stablecoin = StablecoinTicker.of(entity.getStablecoin()); + } + + return new ChainTransfer( + entity.getTransferId(), + entity.getPaymentId(), + entity.getCorrelationId(), + entity.getTransferType(), + entity.getParentTransferId(), + chainId, + stablecoin, + entity.getAmount(), + entity.getFromWalletId(), + entity.getToAddress(), + entity.getFromAddress(), + entity.getNonce(), + entity.getTxHash(), + entity.getStatus(), + entity.getBlockNumber(), + entity.getBlockConfirmedAt(), + entity.getConfirmations(), + entity.getGasUsed(), + entity.getGasPriceGwei(), + entity.getAttemptCount(), + entity.getFailureReason(), + entity.getErrorCode(), + entity.getCreatedAt(), + entity.getUpdatedAt() + ); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/TransferLifecycleEventPersistenceMapper.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/TransferLifecycleEventPersistenceMapper.java new file mode 100644 index 00000000..53a71220 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/TransferLifecycleEventPersistenceMapper.java @@ -0,0 +1,37 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.mapper; + +import com.stablecoin.payments.custody.domain.model.TransferLifecycleEvent; +import com.stablecoin.payments.custody.infrastructure.persistence.entity.TransferLifecycleEventEntity; +import org.mapstruct.Mapper; + +@Mapper +public interface TransferLifecycleEventPersistenceMapper { + + default TransferLifecycleEventEntity toEntity(TransferLifecycleEvent event) { + if (event == null) { + return null; + } + return TransferLifecycleEventEntity.builder() + .eventId(event.eventId()) + .transferId(event.transferId()) + .state(event.state()) + .participantType(event.participantType()) + .address(event.address()) + .occurredAt(event.occurredAt()) + .build(); + } + + default TransferLifecycleEvent toDomain(TransferLifecycleEventEntity entity) { + if (entity == null) { + return null; + } + return new TransferLifecycleEvent( + entity.getEventId(), + entity.getTransferId(), + entity.getState(), + entity.getParticipantType(), + entity.getAddress(), + entity.getOccurredAt() + ); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/TransferParticipantPersistenceMapper.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/TransferParticipantPersistenceMapper.java new file mode 100644 index 00000000..99e18719 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/TransferParticipantPersistenceMapper.java @@ -0,0 +1,39 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.mapper; + +import com.stablecoin.payments.custody.domain.model.TransferParticipant; +import com.stablecoin.payments.custody.infrastructure.persistence.entity.TransferParticipantEntity; +import org.mapstruct.Mapper; + +@Mapper +public interface TransferParticipantPersistenceMapper { + + default TransferParticipantEntity toEntity(TransferParticipant participant) { + if (participant == null) { + return null; + } + return TransferParticipantEntity.builder() + .participantId(participant.participantId()) + .transferId(participant.transferId()) + .participantType(participant.participantType()) + .address(participant.address()) + .walletId(participant.walletId()) + .amount(participant.amount()) + .assetCode(participant.assetCode()) + .build(); + } + + default TransferParticipant toDomain(TransferParticipantEntity entity) { + if (entity == null) { + return null; + } + return new TransferParticipant( + entity.getParticipantId(), + entity.getTransferId(), + entity.getParticipantType(), + entity.getAddress(), + entity.getWalletId(), + entity.getAmount(), + entity.getAssetCode() + ); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletBalanceEntityUpdater.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletBalanceEntityUpdater.java new file mode 100644 index 00000000..6db45036 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletBalanceEntityUpdater.java @@ -0,0 +1,21 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.mapper; + +import com.stablecoin.payments.custody.domain.model.WalletBalance; +import com.stablecoin.payments.custody.infrastructure.persistence.entity.WalletBalanceEntity; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +@Mapper +public interface WalletBalanceEntityUpdater { + + default void updateEntity(@MappingTarget WalletBalanceEntity entity, WalletBalance balance) { + if (balance == null) { + return; + } + entity.setAvailableBalance(balance.availableBalance()); + entity.setReservedBalance(balance.reservedBalance()); + entity.setBlockchainBalance(balance.blockchainBalance()); + entity.setLastIndexedBlock(balance.lastIndexedBlock()); + entity.setUpdatedAt(balance.updatedAt()); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletBalancePersistenceMapper.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletBalancePersistenceMapper.java new file mode 100644 index 00000000..56e35272 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletBalancePersistenceMapper.java @@ -0,0 +1,57 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.mapper; + +import com.stablecoin.payments.custody.domain.model.ChainId; +import com.stablecoin.payments.custody.domain.model.StablecoinTicker; +import com.stablecoin.payments.custody.domain.model.WalletBalance; +import com.stablecoin.payments.custody.infrastructure.persistence.entity.WalletBalanceEntity; +import org.mapstruct.Mapper; + +@Mapper +public interface WalletBalancePersistenceMapper { + + default WalletBalanceEntity toEntity(WalletBalance balance) { + if (balance == null) { + return null; + } + return WalletBalanceEntity.builder() + .balanceId(balance.balanceId()) + .walletId(balance.walletId()) + .chainId(balance.chainId() != null ? balance.chainId().value() : null) + .stablecoin(balance.stablecoin() != null ? balance.stablecoin().ticker() : null) + .availableBalance(balance.availableBalance()) + .reservedBalance(balance.reservedBalance()) + .blockchainBalance(balance.blockchainBalance()) + .lastIndexedBlock(balance.lastIndexedBlock()) + .updatedAt(balance.updatedAt()) + .build(); + } + + default WalletBalance toDomain(WalletBalanceEntity entity) { + if (entity == null) { + return null; + } + + ChainId chainId = null; + if (entity.getChainId() != null) { + chainId = new ChainId(entity.getChainId()); + } + + StablecoinTicker stablecoin = null; + if (entity.getStablecoin() != null) { + stablecoin = StablecoinTicker.of(entity.getStablecoin()); + } + + return new WalletBalance( + entity.getBalanceId(), + entity.getWalletId(), + chainId, + stablecoin, + entity.getAvailableBalance(), + entity.getReservedBalance(), + entity.getBlockchainBalance(), + entity.getLastIndexedBlock(), + entity.getVersion() != null ? entity.getVersion() : 0L, + entity.getUpdatedAt() + ); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletEntityUpdater.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletEntityUpdater.java new file mode 100644 index 00000000..f99a9051 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletEntityUpdater.java @@ -0,0 +1,18 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.mapper; + +import com.stablecoin.payments.custody.domain.model.Wallet; +import com.stablecoin.payments.custody.infrastructure.persistence.entity.WalletEntity; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +@Mapper +public interface WalletEntityUpdater { + + default void updateEntity(@MappingTarget WalletEntity entity, Wallet wallet) { + if (wallet == null) { + return; + } + entity.setActive(wallet.active()); + entity.setDeactivatedAt(wallet.deactivatedAt()); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletPersistenceMapper.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletPersistenceMapper.java new file mode 100644 index 00000000..9086126e --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/persistence/mapper/WalletPersistenceMapper.java @@ -0,0 +1,62 @@ +package com.stablecoin.payments.custody.infrastructure.persistence.mapper; + +import com.stablecoin.payments.custody.domain.model.ChainId; +import com.stablecoin.payments.custody.domain.model.StablecoinTicker; +import com.stablecoin.payments.custody.domain.model.Wallet; +import com.stablecoin.payments.custody.infrastructure.persistence.entity.WalletEntity; +import org.mapstruct.Mapper; + +@Mapper +public interface WalletPersistenceMapper { + + default WalletEntity toEntity(Wallet wallet) { + if (wallet == null) { + return null; + } + return WalletEntity.builder() + .walletId(wallet.walletId()) + .chainId(wallet.chainId() != null ? wallet.chainId().value() : null) + .address(wallet.address()) + .addressChecksum(wallet.addressChecksum()) + .tier(wallet.tier()) + .purpose(wallet.purpose()) + .custodian(wallet.custodian()) + .vaultAccountId(wallet.vaultAccountId()) + .stablecoin(wallet.stablecoin() != null ? wallet.stablecoin().ticker() : null) + .active(wallet.active()) + .createdAt(wallet.createdAt()) + .deactivatedAt(wallet.deactivatedAt()) + .build(); + } + + default Wallet toDomain(WalletEntity entity) { + if (entity == null) { + return null; + } + + ChainId chainId = null; + if (entity.getChainId() != null) { + chainId = new ChainId(entity.getChainId()); + } + + StablecoinTicker stablecoin = null; + if (entity.getStablecoin() != null) { + stablecoin = StablecoinTicker.of(entity.getStablecoin()); + } + + return new Wallet( + entity.getWalletId(), + chainId, + entity.getAddress(), + entity.getAddressChecksum(), + entity.getTier(), + entity.getPurpose(), + entity.getCustodian(), + entity.getVaultAccountId(), + stablecoin, + entity.isActive(), + entity.getCreatedAt(), + entity.getDeactivatedAt() + ); + } +} diff --git a/blockchain-custody/blockchain-custody/src/main/resources/db/migration/V3__fix_chain_id_nullable.sql b/blockchain-custody/blockchain-custody/src/main/resources/db/migration/V3__fix_chain_id_nullable.sql new file mode 100644 index 00000000..3e03da09 --- /dev/null +++ b/blockchain-custody/blockchain-custody/src/main/resources/db/migration/V3__fix_chain_id_nullable.sql @@ -0,0 +1,7 @@ +-- ============================================================ +-- S4 Blockchain & Custody — Fix chain_id nullable for PENDING transfers +-- ChainTransfer starts in PENDING state with chain_id = null; +-- chain_id is set during selectChain() transition to CHAIN_SELECTED. +-- ============================================================ + +ALTER TABLE chain_transfers ALTER COLUMN chain_id DROP NOT NULL;