Skip to content

feat(s4): application controllers + TransferCommandHandler (STA-143)#147

Merged
Puneethkumarck merged 3 commits into
mainfrom
feature/STA-143-s4-controllers-transfer-command-handler
Mar 9, 2026
Merged

feat(s4): application controllers + TransferCommandHandler (STA-143)#147
Puneethkumarck merged 3 commits into
mainfrom
feature/STA-143-s4-controllers-transfer-command-handler

Conversation

@Puneethkumarck

@Puneethkumarck Puneethkumarck commented Mar 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • TransferController: REST API — POST /v1/transfers (202 Accepted / 200 OK idempotent), GET /v1/transfers/{id}, GET /v1/wallets/{id}/balance
  • TransferCommandHandler: Domain command handler orchestrating the full transfer pipeline — chain selection → balance reservation (pessimistic lock) → nonce acquisition → custody signing → submit + outbox event
  • GlobalExceptionHandler: Maps domain exceptions to BC-XXXX error codes (BC-1001 insufficient balance, BC-1003 transfer not found, BC-1004 custody signing, BC-1005 wallet not found)
  • CustodyOutboxEventPublisher + CustodyOutboxHandler: Namastack outbox pattern for TransferSubmittedEvent Kafka publishing
  • TransferRequest DTO with jakarta validation, TransferResult wrapper record, Feign client updated with submitTransfer()
  • 21 new tests (11 handler unit + 10 controller MockMvc), 396 total S4 tests green

Test plan

  • 11 TransferCommandHandlerTest: forward pipeline, idempotent replay, insufficient balance, no wallet, custody signing failure, INPUT/OUTPUT participants, lifecycle events, getTransfer/getWalletBalance found/not found
  • 10 TransferControllerTest: 202 new transfer, 200 idempotent, 400 validation, 422 insufficient balance, 500 custody error, GET transfer details, 404 not found, 400 invalid UUID, wallet balance, 404 wallet not found
  • All 396 S4 tests pass
  • Spotless check clean

Closes STA-143

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • New endpoints to submit transfers, check transfer status, and query wallet balances with idempotency support.
  • Error Handling
    • Centralized exception handling with standardized error codes for validation, balance, chain, signing, and not-found scenarios.
  • Infrastructure
    • Outbox→Kafka event publishing added for reliable asynchronous transfer events.
  • Tests
    • Expanded unit and controller test suites covering success, idempotency, and error paths.

@Puneethkumarck Puneethkumarck added phase-3 Value Movement MVP service-s4 Blockchain feature New feature labels Mar 9, 2026
REST API layer and domain command handler for blockchain transfer orchestration:
- TransferController: POST /v1/transfers (202/200 idempotent), GET /v1/transfers/{id}, GET /v1/wallets/{id}/balance
- TransferCommandHandler: chain selection → balance reservation → nonce → custody signing → submit pipeline
- GlobalExceptionHandler: BC-XXXX error codes (1001 insufficient balance, 1003 not found, 1004 signing, 1005 wallet)
- CustodyOutboxEventPublisher + CustodyOutboxHandler: Namastack outbox for TransferSubmittedEvent
- TransferRequest DTO with jakarta validation, TransferResult wrapper, Feign client update
- 21 new tests (11 handler + 10 controller), 396 total S4 tests green

Closes STA-143

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Puneethkumarck Puneethkumarck force-pushed the feature/STA-143-s4-controllers-transfer-command-handler branch from 874d0c2 to a840c87 Compare March 9, 2026 20:57
@coderabbitai

coderabbitai Bot commented Mar 9, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds a complete transfer submission flow: API model (TransferRequest), Feign client method, REST controller, TransferCommandHandler orchestration, domain exceptions/results, outbox-based event publishing and Kafka handler, global exception mapping, and comprehensive unit & controller tests.

Changes

Cohort / File(s) Summary
API model
blockchain-custody/blockchain-custody-api/src/main/java/com/stablecoin/payments/custody/api/TransferRequest.java
New immutable TransferRequest record with 8 fields and Jakarta Bean Validation annotations (UUIDs, not-blank strings, decimal pattern and @DecimalMin for amount).
Client
blockchain-custody/blockchain-custody-client/src/main/java/com/stablecoin/payments/custody/client/BlockchainCustodyClient.java
Added submitTransfer(TransferRequest) Feign POST mapping to /v1/transfers returning TransferResponse.
Controller & API
blockchain-custody/blockchain-custody/src/main/java/.../application/controller/TransferController.java, .../GlobalExceptionHandler.java
New TransferController with POST /v1/transfers, GET /v1/transfers/{id}, GET /v1/wallets/{id}/balance; GlobalExceptionHandler centralizes mapping domain/validation exceptions to ApiError with codes.
Domain service & model
.../domain/service/TransferCommandHandler.java, .../domain/model/TransferResult.java
New TransferCommandHandler orchestrates idempotent transfer initiation, chain selection, wallet lookup, pessimistic balance reservation, custody signing, state transitions, persistence, and event publication. TransferResult wraps transfer + created flag.
Domain exceptions
.../domain/exception/{InsufficientBalanceException, ChainUnavailableException, CustodySigningException, TransferNotFoundException, WalletNotFoundException}.java
Five new runtime exceptions with standardized ERROR_CODE constants for domain error handling.
Outbox & messaging
.../infrastructure/messaging/{CustodyOutboxEventPublisher, CustodyOutboxHandler}.java
CustodyOutboxEventPublisher schedules events into Namastack Outbox using reflection to resolve paymentId/transferId key; CustodyOutboxHandler publishes outbox records to Kafka using static TOPIC reflection and 10s send timeout.
Tests — controller
blockchain-custody/blockchain-custody/src/test/java/.../application/controller/TransferControllerTest.java
WebMvcTest covering POST/GET endpoints: success (202 for created, 200 for replay), validation failures, domain errors (insufficient balance, custody signing, chain unavailable), and not-found cases.
Tests — service
blockchain-custody/blockchain-custody/src/test/java/.../domain/service/TransferCommandHandlerTest.java
Extensive unit tests for full transfer pipeline, idempotency, error paths, lifecycle/participant recording, and wallet balance queries.
Test fixtures & utils
blockchain-custody/blockchain-custody/src/testFixtures/java/.../fixtures/{ChainSelectionFixtures, TestUtils}.java
Added chain selection fixtures (BASE_CANDIDATES, aBaseSelectionResult) and BigDecimal comparator in TestUtils for assertions.

Sequence Diagram

sequenceDiagram
    participant Client
    participant TransferController
    participant TransferCommandHandler
    participant ChainSelectionEngine
    participant WalletRepository
    participant CustodyEngine
    participant Outbox
    participant KafkaPublisher

    Client->>TransferController: POST /v1/transfers (TransferRequest)
    TransferController->>TransferCommandHandler: initiateTransfer(...)
    
    TransferCommandHandler->>TransferCommandHandler: Check idempotency by paymentId
    alt Idempotent replay
        TransferCommandHandler-->>TransferController: TransferResult(created=false)
    else New transfer
        TransferCommandHandler->>ChainSelectionEngine: selectChain(...)
        ChainSelectionEngine-->>TransferCommandHandler: ChainSelectionResult

        TransferCommandHandler->>WalletRepository: findActiveWallet(...)
        WalletRepository-->>TransferCommandHandler: Wallet

        TransferCommandHandler->>TransferCommandHandler: Reserve balance (pessimistic lock)

        TransferCommandHandler->>CustodyEngine: signAndSubmit(chainTransfer)
        alt Signing success
            CustodyEngine-->>TransferCommandHandler: txHash
            TransferCommandHandler->>Outbox: schedule(TransferSubmittedEvent)
            Outbox-->>TransferCommandHandler: scheduled
        else Signing failure
            CustodyEngine-->>TransferCommandHandler: exception
            TransferCommandHandler->>TransferCommandHandler: release reserved balance
            TransferCommandHandler-->>TransferController: throw CustodySigningException
        end

        TransferCommandHandler->>TransferCommandHandler: persist transfer state
        TransferCommandHandler-->>TransferController: TransferResult(created=true)
    end

    Outbox->>KafkaPublisher: publish(event,key)
    KafkaPublisher-->>Outbox: ack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

test

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the main change: adding application controllers and TransferCommandHandler for S4 service with issue reference STA-143.
Description check ✅ Passed Description covers all critical sections: summary of key changes, test plan with counts, spotless validation, and issue closure. All required template sections are addressed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/STA-143-s4-controllers-transfer-command-handler

Comment @coderabbitai help to get the list of available commands and usage tips.

- Remove assertThat on handler return values — use interaction verification only
- Fix single-assert pattern in getWalletBalance test (build expected + recursive compare)
- Move aSelectionResult() factory to ChainSelectionFixtures (shared testFixtures)
- Add ERROR_CODE constant to ChainUnavailableException, use in GlobalExceptionHandler
- Fix inline FQNs in WalletBalanceDetails record (Wallet, List imports)
- Fix inline java.time.Instant FQN in TransferController

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@blockchain-custody/blockchain-custody-api/src/main/java/com/stablecoin/payments/custody/api/TransferRequest.java`:
- Around line 24-26: The amount field in TransferRequest currently uses
`@NotBlank` and `@DecimalMin` on a String which yields misleading errors for
non-numeric input; add a `@Pattern`(regexp="^\\d+(\\.\\d+)?$", message="amount
must be a valid decimal number") on the amount field (keeping the existing
`@NotBlank` and `@DecimalMin`) so malformed values (e.g., "abc") produce a clear
format validation error; update the annotation list on the amount property in
TransferRequest accordingly.
- Around line 28-29: The TransferRequest DTO's toWalletAddress field only has
`@NotBlank`; add basic format validation to reject obvious garbage by applying a
validation constraint (e.g., a `@Pattern` or a custom validator) on the
TransferRequest.toWalletAddress field that enforces sensible character and
length rules for blockchain addresses (e.g., hex prefix optional, allowed hex
chars and length range, or alphanumeric constraints for non-hex chains); ensure
the new validator message is descriptive and note in the change that downstream
chain-specific validation must still run before submission (so keep downstream
validation intact).

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java`:
- Around line 87-93: The handler handleCustodySigning in GlobalExceptionHandler
currently logs ex.getMessage(), which may leak custody-sensitive details; change
the logging to avoid raw exception messages by logging only a sanitized summary
such as the exception class name and a short safe context (e.g.
CustodySigningException or ex.getClass().getSimpleName()) or a fixed message via
log.error("Custody signing error: {}", ex.getClass().getSimpleName()) and ensure
the ApiError returned (from ApiError.of using
CustodySigningException.ERROR_CODE) does not include any sensitive details from
ex.getMessage().

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/TransferController.java`:
- Around line 109-116: The estimatedConfirmationSeconds method currently
hardcodes chain times; replace it by injecting a configuration-backed
Map<String,Integer> (e.g., a `@ConfigurationProperties` class like
ChainConfirmationsProperties with a Map<String,Integer> confirmations) into
TransferController and change estimatedConfirmationSeconds(String chainId) to
look up chainId in that map (with a sensible default/fallback when absent).
Add/define the properties prefix and YAML/props entries for existing chains,
validate/convert values to Integer as needed, and update any callers to use the
new lookup method in TransferController.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/domain/service/TransferCommandHandler.java`:
- Around line 194-202: The catch in releaseBalance currently swallows all
exceptions from reservedBalance.release(...) or
walletBalanceRepository.save(...), risking permanent locked funds; change it to
surface failures by removing silent swallowing and either rethrowing the caught
exception (or wrap and throw a domain-specific exception) from releaseBalance so
the transaction fails, and/or invoke a compensation/alert mechanism (e.g.,
enqueue a retry/compensation job or call an alerting service) when
walletBalanceRepository.save(...) fails; locate the releaseBalance method in
TransferCommandHandler and adjust error handling around reservedBalance.release
and walletBalanceRepository.save accordingly.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/messaging/CustodyOutboxEventPublisher.java`:
- Around line 31-45: The resolveKey method currently uses reflection and
String.valueOf(...) which converts null to the string "null", catches broad
Exception types, and discards the first exception; update resolveKey to (1)
explicitly look up and invoke the accessor methods "paymentId" and then
"transferId" and check the returned value for null (throw
IllegalArgumentException if accessor returns null rather than producing the
literal "null"), (2) catch and handle specific reflection exceptions
(NoSuchMethodException, IllegalAccessException, InvocationTargetException),
unwrap InvocationTargetException to surface the underlying cause, and (3) when
falling back from the paymentId attempt to transferId, add the first exception
(e1) as a suppressed exception to the final IllegalArgumentException so
diagnostics include both failures; keep the method name resolveKey and the same
failure message but include suppressed/underlying exceptions for clarity.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/messaging/CustodyOutboxHandler.java`:
- Around line 27-35: The catch-all in CustodyOutboxHandler around
kafkaTemplate.send(...).get(...) swallows InterruptedException; update the
exception handling to catch InterruptedException separately, call
Thread.currentThread().interrupt() to restore the thread's interrupt status, log
the interruption (referencing kafkaTemplate.send,
event.getClass().getSimpleName(), topic, key), and then rethrow/wrap the
InterruptedException as the cause in the RuntimeException (as currently done for
other exceptions) so interruption is not lost; keep the existing broad Exception
catch for other failures but ensure InterruptedException handling is prioritized
and preserves the original exception as the cause.

In
`@blockchain-custody/blockchain-custody/src/test/java/com/stablecoin/payments/custody/application/controller/TransferControllerTest.java`:
- Around line 52-173: Add a test in TransferControllerTest that mocks
transferCommandHandler.initiateTransfer to throw new
ChainUnavailableException(...) and asserts the controller returns HTTP 503 and
error code "BC-1002"; specifically create a test similar to existing ones (e.g.,
shouldReturn503ForChainUnavailable) that calls mockMvc.post("/v1/transfers")
with the same JSON payload used in other tests, uses
given(transferCommandHandler.initiateTransfer(...)).willThrow(new
ChainUnavailableException(...)), and asserts status().isServiceUnavailable() and
jsonPath("$.code").value("BC-1002").

In
`@blockchain-custody/blockchain-custody/src/testFixtures/java/com/stablecoin/payments/custody/fixtures/ChainSelectionFixtures.java`:
- Around line 90-96: The fixture aBaseSelectionResult() creates a
non-deterministic transferId with UUID.randomUUID(), causing inconsistent test
behavior; change it to use the deterministic TRANSFER_ID constant (same as used
by aSelectionRequest()) when building the ChainSelectionResult (i.e., replace
the transferId builder argument to use TRANSFER_ID) so all fixtures share the
same stable ID.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bc5f67bf-94fe-45a9-a277-23e2d289db83

📥 Commits

Reviewing files that changed from the base of the PR and between 0e52500 and 219c3d9.

📒 Files selected for processing (17)
  • blockchain-custody/blockchain-custody-api/src/main/java/com/stablecoin/payments/custody/api/TransferRequest.java
  • blockchain-custody/blockchain-custody-client/src/main/java/com/stablecoin/payments/custody/client/BlockchainCustodyClient.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/TransferController.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/domain/exception/ChainUnavailableException.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/domain/exception/CustodySigningException.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/domain/exception/InsufficientBalanceException.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/domain/exception/TransferNotFoundException.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/domain/exception/WalletNotFoundException.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/domain/model/TransferResult.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/domain/service/TransferCommandHandler.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/messaging/CustodyOutboxEventPublisher.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/messaging/CustodyOutboxHandler.java
  • blockchain-custody/blockchain-custody/src/test/java/com/stablecoin/payments/custody/application/controller/TransferControllerTest.java
  • blockchain-custody/blockchain-custody/src/test/java/com/stablecoin/payments/custody/domain/service/TransferCommandHandlerTest.java
  • blockchain-custody/blockchain-custody/src/testFixtures/java/com/stablecoin/payments/custody/fixtures/ChainSelectionFixtures.java
  • blockchain-custody/blockchain-custody/src/testFixtures/java/com/stablecoin/payments/custody/fixtures/TestUtils.java

Comment on lines +28 to +29
@NotBlank(message = "toWalletAddress is required")
String toWalletAddress,

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

toWalletAddress lacks format validation.

Currently accepts any non-blank string. Multi-chain address validation is complex, but a basic length/character constraint could catch obvious garbage early. Verify downstream validation handles this before submission.

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

In
`@blockchain-custody/blockchain-custody-api/src/main/java/com/stablecoin/payments/custody/api/TransferRequest.java`
around lines 28 - 29, The TransferRequest DTO's toWalletAddress field only has
`@NotBlank`; add basic format validation to reject obvious garbage by applying a
validation constraint (e.g., a `@Pattern` or a custom validator) on the
TransferRequest.toWalletAddress field that enforces sensible character and
length rules for blockchain addresses (e.g., hex prefix optional, allowed hex
chars and length range, or alphanumeric constraints for non-hex chains); ensure
the new validator message is descriptive and note in the change that downstream
chain-specific validation must still run before submission (so keep downstream
validation intact).

Comment on lines +109 to +116
private Integer estimatedConfirmationSeconds(String chainId) {
return switch (chainId) {
case "base" -> 12;
case "ethereum" -> 300;
case "solana" -> 5;
default -> null;
};
}

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 externalizing chain confirmation times to configuration.

Hardcoded values work for now, but these will need updating when adding new chains or if block times change. A Map<String, Integer> from @ConfigurationProperties would be more maintainable.

🤖 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/application/controller/TransferController.java`
around lines 109 - 116, The estimatedConfirmationSeconds method currently
hardcodes chain times; replace it by injecting a configuration-backed
Map<String,Integer> (e.g., a `@ConfigurationProperties` class like
ChainConfirmationsProperties with a Map<String,Integer> confirmations) into
TransferController and change estimatedConfirmationSeconds(String chainId) to
look up chainId in that map (with a sensible default/fallback when absent).
Add/define the properties prefix and YAML/props entries for existing chains,
validate/convert values to Integer as needed, and update any callers to use the
new lookup method in TransferController.

Comment on lines +31 to +45
private String resolveKey(Object event) {
try {
var method = event.getClass().getMethod("paymentId");
return String.valueOf(method.invoke(event));
} catch (Exception e1) {
try {
var method = event.getClass().getMethod("transferId");
return String.valueOf(method.invoke(event));
} catch (Exception e2) {
throw new IllegalArgumentException(
"Event class missing accessor for 'paymentId' or 'transferId': "
+ event.getClass().getName(), e2);
}
}
}

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

Reflection-based key resolution has null-safety and exception-handling gaps.

  1. String.valueOf(method.invoke(event)) returns "null" if the accessor returns null, which would produce a problematic outbox key.
  2. Catching generic Exception masks distinct failure modes (e.g., InvocationTargetException wrapping a runtime exception thrown by the accessor vs. NoSuchMethodException).
  3. e1 is discarded when attempting the transferId fallback—consider adding it as a suppressed exception for diagnostics.
Proposed improvement for null-safety and exception clarity
 private String resolveKey(Object event) {
+    Object key = tryInvoke(event, "paymentId");
+    if (key == null) {
+        key = tryInvoke(event, "transferId");
+    }
+    if (key == null) {
+        throw new IllegalArgumentException(
+                "Event class missing non-null accessor for 'paymentId' or 'transferId': "
+                        + event.getClass().getName());
+    }
+    return key.toString();
+}
+
+private Object tryInvoke(Object event, String methodName) {
     try {
-        var method = event.getClass().getMethod("paymentId");
-        return String.valueOf(method.invoke(event));
-    } catch (Exception e1) {
-        try {
-            var method = event.getClass().getMethod("transferId");
-            return String.valueOf(method.invoke(event));
-        } catch (Exception e2) {
-            throw new IllegalArgumentException(
-                    "Event class missing accessor for 'paymentId' or 'transferId': "
-                            + event.getClass().getName(), e2);
-        }
+        var method = event.getClass().getMethod(methodName);
+        return method.invoke(event);
+    } catch (NoSuchMethodException e) {
+        return null;
+    } catch (ReflectiveOperationException e) {
+        throw new IllegalArgumentException("Failed to invoke " + methodName + " on " + event.getClass().getName(), e);
     }
 }
📝 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
private String resolveKey(Object event) {
try {
var method = event.getClass().getMethod("paymentId");
return String.valueOf(method.invoke(event));
} catch (Exception e1) {
try {
var method = event.getClass().getMethod("transferId");
return String.valueOf(method.invoke(event));
} catch (Exception e2) {
throw new IllegalArgumentException(
"Event class missing accessor for 'paymentId' or 'transferId': "
+ event.getClass().getName(), e2);
}
}
}
private String resolveKey(Object event) {
Object key = tryInvoke(event, "paymentId");
if (key == null) {
key = tryInvoke(event, "transferId");
}
if (key == null) {
throw new IllegalArgumentException(
"Event class missing non-null accessor for 'paymentId' or 'transferId': "
event.getClass().getName());
}
return key.toString();
}
private Object tryInvoke(Object event, String methodName) {
try {
var method = event.getClass().getMethod(methodName);
return method.invoke(event);
} catch (NoSuchMethodException e) {
return null;
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("Failed to invoke " + methodName + " on " + event.getClass().getName(), e);
}
}
🤖 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/messaging/CustodyOutboxEventPublisher.java`
around lines 31 - 45, The resolveKey method currently uses reflection and
String.valueOf(...) which converts null to the string "null", catches broad
Exception types, and discards the first exception; update resolveKey to (1)
explicitly look up and invoke the accessor methods "paymentId" and then
"transferId" and check the returned value for null (throw
IllegalArgumentException if accessor returns null rather than producing the
literal "null"), (2) catch and handle specific reflection exceptions
(NoSuchMethodException, IllegalAccessException, InvocationTargetException),
unwrap InvocationTargetException to surface the underlying cause, and (3) when
falling back from the paymentId attempt to transferId, add the first exception
(e1) as a suppressed exception to the final IllegalArgumentException so
diagnostics include both failures; keep the method name resolveKey and the same
failure message but include suppressed/underlying exceptions for clarity.

- Remove silent exception swallowing in releaseBalance — let failure
  propagate to fail the transaction and prevent permanent balance lock
- Handle InterruptedException separately in CustodyOutboxHandler —
  restore thread interrupt status before rethrowing
- Sanitize custody signing error log — log class name instead of
  message which may contain vault IDs or provider details
- Add @pattern validation on TransferRequest.amount for clear format
  error on non-numeric input
- Add 503 ChainUnavailableException controller test (BC-1002)
- Use deterministic TRANSFER_ID in aBaseSelectionResult() fixture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Puneethkumarck Puneethkumarck merged commit b0d1047 into main Mar 9, 2026
9 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature phase-3 Value Movement MVP service-s4 Blockchain

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant