68: Await CONFIRMED on-chain outcomes for Jupiter Swap and Perps operations
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Issue #67 introduced a generic, blocking `SolanaBlockChain.awaitTransaction()` operation with explicit commitment, timeout, and outcome semantics. Jupiter Swap and Perps already own their respective build, validation, signing, and single provider-submission flows, so they are the correct boundaries for adding independent Solana confirmation. Their public APIs and implementations are already package-separated even though they currently share this repository.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Add one consistent Jupiter-level representation for definitive on-chain failure and unknown timeout.
|
||||||
|
- Keep provider-response validation visibly separate from the subsequent independent Solana confirmation.
|
||||||
|
- Preserve existing constructors and operation return types while making confirmation timeout an injectable high-level policy.
|
||||||
|
- Keep increase and decrease confirmation behavior identical and keep alarm reporting outcome-aware.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Changing blockchain awaiting, wallet submission, transaction construction, signing, Jupiter provider submission, or financial validation.
|
||||||
|
- Adding retries, resubmission, persistent reconciliation, configuration changes, or automated tests.
|
||||||
|
- Integrating confirmation into burn, Evelyn, Discord, or other consumers.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Put the outcome exception in the common Jupiter API package
|
||||||
|
|
||||||
|
`JupiterTransactionOutcomeException` will be a checked public exception in `com.r35157.libs.jupiter`, shared by Swap and Perps without making either API depend on the other. It will defensively validate and retain the signature, requested commitment, and complete `SolanaTransactionOutcome`. Its message will derive definitive-failure or unknown-timeout diagnostics from that structured state.
|
||||||
|
|
||||||
|
An exception local to each service was rejected because it would duplicate identical cross-provider outcome semantics and prevent callers such as alarm actions from applying one policy.
|
||||||
|
|
||||||
|
### Confirm only after complete provider validation
|
||||||
|
|
||||||
|
Swap will build its `JupiterSwapResult` only after all existing `/execute` status, code, signature, and actual-amount checks pass, then await the result signature. Perps will await only after its existing execution response yields a valid non-blank `txid`. This maintains two explicit trust boundaries: Jupiter validates its response first; Solana independently establishes the on-chain outcome second.
|
||||||
|
|
||||||
|
Treating provider success as implicit confirmation was rejected because it is the gap this change closes. Replacing provider validation with Solana confirmation was rejected because on-chain status cannot validate Jupiter's reported token amounts or response contract.
|
||||||
|
|
||||||
|
### Keep timeout policy immutable in each service instance
|
||||||
|
|
||||||
|
Each implementation will store a validated `Duration`. Existing constructors delegate to overloads using a two-minute constant. The duration is passed unchanged to each `awaitTransaction()` call at `CONFIRMED`, so pre-confirmation work does not consume it.
|
||||||
|
|
||||||
|
A configuration-file setting was rejected because issue #68 deliberately defines constructor policy only. A timeout in wallet or blockchain submission was rejected because confirmation belongs to the high-level operation.
|
||||||
|
|
||||||
|
### Centralize confirmation policy at the smallest useful scope
|
||||||
|
|
||||||
|
Perps increase and decrease will call one private helper that invokes `awaitTransaction()` and switches on all three outcomes. Swap will use its own equivalent private helper because the implementations are separate future artifacts; sharing implementation mechanics across them would create an unnecessary coupling.
|
||||||
|
|
||||||
|
Both helpers wrap only confirmation `IOException`s with operation, signature, and unknown-outcome context, preserve the cause, allow `InterruptedException` to propagate directly, and never retry. This avoids accidentally changing existing pre-signature/provider error behavior.
|
||||||
|
|
||||||
|
### Keep alarms presentation-only for confirmation results
|
||||||
|
|
||||||
|
The two alarm actions will catch `JupiterTransactionOutcomeException` before their generic error handling and format `FAILED` and `TIMED_OUT` separately. They will not call the blockchain API or attempt reconciliation, preserving the Jupiter service as owner of complete operation orchestration.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [A provider submission can succeed while confirmation communication fails] → Preserve the known signature, report the on-chain outcome as unknown, retain the cause, and prohibit automatic resubmission.
|
||||||
|
- [Synchronous confirmation extends operation latency] → Use an immutable configurable timeout with a safe two-minute default and preserve interruption.
|
||||||
|
- [Alarm reporting is transient and not reconciled] → Clearly label timeout as unknown; persistent reconciliation remains explicitly deferred.
|
||||||
|
- [Checked exception propagation affects callers at compile time] → Update both public service contracts and every affected call site in this repository consistently.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Add the common checked exception and update both public service contracts.
|
||||||
|
2. Add compatible implementation constructor overloads and confirmation helpers.
|
||||||
|
3. Update alarm actions for structured outcome reporting.
|
||||||
|
4. Compile main and test source sets, run available non-live checks, and retain the active OpenSpec change for review.
|
||||||
|
|
||||||
|
Rollback consists of reverting this change as one unit; no data or configuration migration is involved.
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Jupiter's provider responses currently end Swap and Perps operations without independently confirming the returned transaction signature on Solana. The shared `SolanaBlockChain.awaitTransaction()` API now makes it possible for these high-level operations to report success only after `CONFIRMED` on-chain success while preserving definitive failure and unknown-timeout outcomes.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a shared checked Jupiter outcome exception that preserves the submitted signature, requested commitment, and complete failed or timed-out Solana outcome.
|
||||||
|
- Make Jupiter Swap independently await its validated execution signature at `CONFIRMED` before returning its existing result.
|
||||||
|
- Make Jupiter Perps increase and decrease independently await their valid execution signatures at `CONFIRMED` through one common private policy helper.
|
||||||
|
- Add validated constructor-injected confirmation timeouts while preserving existing constructors with a two-minute default.
|
||||||
|
- Preserve Jupiter provider-response validation as a distinct prerequisite to Solana confirmation, including Swap actual-amount validation.
|
||||||
|
- Add outcome-specific Perps alarm reporting without moving confirmation into alarm actions or introducing retries or resubmission.
|
||||||
|
- Update affected public Javadoc and checked-exception declarations.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `jupiter-perps-transaction-confirmation`: Defines independent on-chain confirmation and alarm reporting for Jupiter Perps increase and decrease operations.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `jupiter-swap-service`: Requires independent `CONFIRMED` Solana success after the existing Jupiter execution-response validation and defines structured failure and timeout handling.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
The Jupiter common API gains one checked exception. The Jupiter Swap and Perps APIs, their reference implementations, and the two Perps alarm actions gain corresponding confirmation and error propagation behavior. Existing transaction construction, signing, provider submission, wallet methods, pacing, and retry behavior remain unchanged.
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
Defines independent Solana confirmation and outcome reporting for completed Jupiter Perps increase and decrease submissions.
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Confirmed Perps operation completion
|
||||||
|
After the existing Perps increase or decrease flow produces a valid non-blank transaction signature, the service SHALL independently await that signature at `CONFIRMED`. It SHALL return the existing signature result only for `SUCCEEDED`; provider response validation and financial validation SHALL remain distinct prerequisites and SHALL not themselves constitute on-chain confirmation.
|
||||||
|
|
||||||
|
#### Scenario: Increase reaches confirmed success
|
||||||
|
- **WHEN** an increase submission returns a valid signature and its independent `CONFIRMED` outcome is `SUCCEEDED`
|
||||||
|
- **THEN** the service returns that signature
|
||||||
|
|
||||||
|
#### Scenario: Decrease reaches confirmed success
|
||||||
|
- **WHEN** a decrease submission returns a valid signature and its independent `CONFIRMED` outcome is `SUCCEEDED`
|
||||||
|
- **THEN** the service returns that signature
|
||||||
|
|
||||||
|
### Requirement: Structured exceptional outcomes
|
||||||
|
The shared Jupiter API SHALL expose a checked outcome exception containing the known non-blank transaction signature, requested commitment, and complete transaction outcome. Construction SHALL reject null or blank signatures, null commitments, null outcomes, and `SUCCEEDED` outcomes. `FAILED` SHALL identify a definitive on-chain failure including its slot and compact Solana error, while `TIMED_OUT` SHALL identify an unknown outcome and explicitly warn against automatic equivalent resubmission.
|
||||||
|
|
||||||
|
#### Scenario: Confirmed on-chain failure
|
||||||
|
- **WHEN** an increase or decrease signature reaches `CONFIRMED` with a `FAILED` outcome
|
||||||
|
- **THEN** the service throws the checked outcome exception preserving the signature, `CONFIRMED`, slot, and complete compact Solana failure details
|
||||||
|
|
||||||
|
#### Scenario: Confirmation timeout
|
||||||
|
- **WHEN** an increase or decrease signature produces a `TIMED_OUT` outcome
|
||||||
|
- **THEN** the service throws the checked outcome exception preserving the signature, `CONFIRMED`, and unknown outcome without automatically resubmitting
|
||||||
|
|
||||||
|
#### Scenario: Successful outcome is supplied to exception
|
||||||
|
- **WHEN** a caller attempts to construct the outcome exception with `SUCCEEDED` or inconsistent null or blank data
|
||||||
|
- **THEN** construction fails immediately
|
||||||
|
|
||||||
|
### Requirement: Perps confirmation timeout policy
|
||||||
|
The reference service SHALL accept a constructor-injected, non-null, strictly positive confirmation timeout and SHALL preserve its existing constructor with a two-minute default. The configured duration SHALL be forwarded unchanged and SHALL begin governing only when awaiting the already submitted signature starts. Increase and decrease SHALL apply the same confirmation and outcome policy.
|
||||||
|
|
||||||
|
#### Scenario: Existing constructor is used
|
||||||
|
- **WHEN** the reference service is constructed without a confirmation timeout
|
||||||
|
- **THEN** each submitted increase or decrease uses a two-minute confirmation timeout
|
||||||
|
|
||||||
|
#### Scenario: Custom timeout is used
|
||||||
|
- **WHEN** the reference service is constructed with a positive duration
|
||||||
|
- **THEN** each submitted increase or decrease forwards exactly that duration to `CONFIRMED` awaiting
|
||||||
|
|
||||||
|
#### Scenario: Invalid timeout is supplied
|
||||||
|
- **WHEN** the reference service is constructed with a null, zero, or negative duration
|
||||||
|
- **THEN** construction fails before any Perps operation can be submitted
|
||||||
|
|
||||||
|
### Requirement: Post-submission communication and interruption policy
|
||||||
|
If independent confirmation throws an I/O error after a signature is known, the service SHALL throw an `IOException` whose message identifies the increase or decrease operation, includes the signature, and states that the on-chain outcome is unknown, retaining the original error as cause. `InterruptedException` SHALL propagate directly without another RPC or Jupiter request. Confirmation SHALL never submit, rebuild, re-sign, retry, or resubmit a transaction.
|
||||||
|
|
||||||
|
#### Scenario: Confirmation communication fails
|
||||||
|
- **WHEN** independent confirmation throws an I/O error for a known Perps signature
|
||||||
|
- **THEN** the service reports the operation, signature, and unknown outcome, preserves the original error, and performs no retry or resubmission
|
||||||
|
|
||||||
|
#### Scenario: Confirmation is interrupted
|
||||||
|
- **WHEN** independent confirmation throws `InterruptedException`
|
||||||
|
- **THEN** interruption propagates directly and no subsequent request occurs
|
||||||
|
|
||||||
|
### Requirement: Outcome-aware Perps alarm reporting
|
||||||
|
Perps increase and decrease alarm actions SHALL rely on the Perps service for confirmation and SHALL distinguish structured `FAILED`, structured `TIMED_OUT`, and other errors. A failed report SHALL include operation, signature, confirmation slot, and compact Solana failure details. A timed-out report SHALL include operation, signature, unknown-outcome status, and that no equivalent transaction was automatically resubmitted. Alarm actions SHALL NOT invoke transaction awaiting themselves.
|
||||||
|
|
||||||
|
#### Scenario: Alarm operation fails on-chain
|
||||||
|
- **WHEN** a Perps alarm action receives a structured `FAILED` outcome exception
|
||||||
|
- **THEN** it reports the operation, signature, confirmation slot, and compact on-chain failure details distinctly from generic errors
|
||||||
|
|
||||||
|
#### Scenario: Alarm operation remains unknown
|
||||||
|
- **WHEN** a Perps alarm action receives a structured `TIMED_OUT` outcome exception
|
||||||
|
- **THEN** it reports the operation, signature, unknown outcome, and absence of automatic equivalent resubmission distinctly from generic errors
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Confirmed execution result validation
|
||||||
|
The service SHALL first accept a Jupiter execution response only when it has status `Success`, explicitly has result code `0`, contains a non-blank transaction signature, and contains positive valid actual total input and output raw amounts. It SHALL convert each actual amount using the independently resolved precision of its mint. After this provider-response validation, the service SHALL independently await the returned signature on Solana at `CONFIRMED` using its configured confirmation timeout and SHALL return the already validated swap result only for a `SUCCEEDED` outcome. Non-success HTTP responses, malformed responses, expired or rejected swaps, failed or contradictory status/code combinations, missing result codes, blank signatures, and invalid result amounts SHALL remain provider failures and SHALL not be represented as successful swaps.
|
||||||
|
|
||||||
|
#### Scenario: Provider response and on-chain confirmation succeed
|
||||||
|
- **WHEN** Jupiter reports status `Success`, code `0`, a signature, and valid actual total input and output amounts, and that signature independently reaches `CONFIRMED` with a `SUCCEEDED` outcome
|
||||||
|
- **THEN** the service returns the signature and those actual amounts in human-readable units
|
||||||
|
|
||||||
|
#### Scenario: Jupiter rejects or fails execution
|
||||||
|
- **WHEN** Jupiter returns a failed status, a missing or non-zero result code, a contradictory status/code combination, expiration, rejection, non-success HTTP status, malformed body, blank signature, or invalid actual amount
|
||||||
|
- **THEN** the service reports an I/O failure without treating provider validation as an on-chain confirmation
|
||||||
|
|
||||||
|
#### Scenario: Provider succeeds but transaction fails on-chain
|
||||||
|
- **WHEN** a fully validated Jupiter execution response contains a signature whose independent `CONFIRMED` outcome is `FAILED`
|
||||||
|
- **THEN** the service throws a structured checked outcome exception preserving the signature, requested commitment, slot, and complete compact Solana failure details
|
||||||
|
|
||||||
|
#### Scenario: Provider succeeds but confirmation times out
|
||||||
|
- **WHEN** a fully validated Jupiter execution response contains a signature whose independent `CONFIRMED` outcome is `TIMED_OUT`
|
||||||
|
- **THEN** the service throws a structured checked outcome exception preserving the signature, requested commitment, and unknown outcome warning and does not automatically resubmit an equivalent transaction
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Swap confirmation timeout policy
|
||||||
|
The reference service SHALL accept a constructor-injected, non-null, strictly positive confirmation timeout and SHALL preserve its existing constructor with a two-minute default. The configured duration SHALL be passed unchanged to independent confirmation after submission and SHALL begin governing only when awaiting the known signature starts.
|
||||||
|
|
||||||
|
#### Scenario: Valid custom timeout
|
||||||
|
- **WHEN** a reference service is constructed with a positive duration and later obtains a validated execution signature
|
||||||
|
- **THEN** it awaits that signature at `CONFIRMED` with exactly the supplied duration
|
||||||
|
|
||||||
|
#### Scenario: Invalid custom timeout
|
||||||
|
- **WHEN** a reference service is constructed with a null, zero, or negative duration
|
||||||
|
- **THEN** construction fails before any operation can be submitted
|
||||||
|
|
||||||
|
### Requirement: Post-submission communication and interruption policy
|
||||||
|
If independent confirmation fails with an I/O error after the signature is known, the service SHALL throw an `IOException` whose message identifies the swap operation, includes the signature, and states that the on-chain outcome is unknown, with the original error retained as its cause. Interruption SHALL propagate directly as `InterruptedException` without another RPC or Jupiter request. Neither condition SHALL trigger rebuilding, re-signing, retrying, or resubmitting a transaction.
|
||||||
|
|
||||||
|
#### Scenario: Confirmation communication fails
|
||||||
|
- **WHEN** independent confirmation throws an I/O error for a known submitted signature
|
||||||
|
- **THEN** the service reports the signature and unknown on-chain outcome while preserving the original error and performs no retry or resubmission
|
||||||
|
|
||||||
|
#### Scenario: Confirmation is interrupted
|
||||||
|
- **WHEN** independent confirmation throws `InterruptedException`
|
||||||
|
- **THEN** that interruption propagates directly and the service performs no subsequent request
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
## 1. Public Jupiter API
|
||||||
|
|
||||||
|
- [x] 1.1 Add the checked, documented `JupiterTransactionOutcomeException` with validated structured signature, commitment, and failed/timed-out outcome access.
|
||||||
|
- [x] 1.2 Update Jupiter Swap and Perps public Javadoc and checked-exception declarations through their API call chains.
|
||||||
|
|
||||||
|
## 2. Jupiter Service Confirmation
|
||||||
|
|
||||||
|
- [x] 2.1 Add a validated confirmation-timeout overload to `JupiterSwapServiceImpl`, retain its two-minute default constructor, and await validated Swap results at `CONFIRMED` without changing provider validation or submission behavior.
|
||||||
|
- [x] 2.2 Add a validated confirmation-timeout overload to `AnchorIdlJupiterPerpsServiceImpl`, retain its two-minute default constructor, and route increase and decrease signatures through one private `CONFIRMED` outcome-policy helper.
|
||||||
|
- [x] 2.3 Ensure both services preserve structured `FAILED` and `TIMED_OUT` outcomes, contextualize confirmation `IOException`s as unknown outcomes, propagate interruption directly, and perform no retry or resubmission.
|
||||||
|
|
||||||
|
## 3. Perps Alarm Reporting
|
||||||
|
|
||||||
|
- [x] 3.1 Update the increase alarm action to report `FAILED` and `TIMED_OUT` distinctly while leaving confirmation ownership in the Perps service.
|
||||||
|
- [x] 3.2 Update the decrease alarm action to report `FAILED` and `TIMED_OUT` distinctly while leaving confirmation ownership in the Perps service.
|
||||||
|
|
||||||
|
## 4. Verification
|
||||||
|
|
||||||
|
- [x] 4.1 Compile main and test source sets without adding tests or executing live Jupiter/Solana transactions.
|
||||||
|
- [x] 4.2 Run available relevant existing tests only if their runtime is non-live and practical.
|
||||||
|
- [x] 4.3 Run strict OpenSpec validation and `git diff --check`.
|
||||||
|
- [x] 4.4 Review the complete diff for checked-exception propagation, unchanged provider validations, exact timeout forwarding, direct interruption propagation, no retry/resubmission, no generated-source edits, and no unrelated changes.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# jupiter-perps-transaction-confirmation Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Defines independent Solana confirmation and outcome reporting for completed Jupiter Perps increase and decrease submissions.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Confirmed Perps operation completion
|
||||||
|
After the existing Perps increase or decrease flow produces a valid non-blank transaction signature, the service SHALL independently await that signature at `CONFIRMED`. It SHALL return the existing signature result only for `SUCCEEDED`; provider response validation and financial validation SHALL remain distinct prerequisites and SHALL not themselves constitute on-chain confirmation.
|
||||||
|
|
||||||
|
#### Scenario: Increase reaches confirmed success
|
||||||
|
- **WHEN** an increase submission returns a valid signature and its independent `CONFIRMED` outcome is `SUCCEEDED`
|
||||||
|
- **THEN** the service returns that signature
|
||||||
|
|
||||||
|
#### Scenario: Decrease reaches confirmed success
|
||||||
|
- **WHEN** a decrease submission returns a valid signature and its independent `CONFIRMED` outcome is `SUCCEEDED`
|
||||||
|
- **THEN** the service returns that signature
|
||||||
|
|
||||||
|
### Requirement: Structured exceptional outcomes
|
||||||
|
The shared Jupiter API SHALL expose a checked outcome exception containing the known non-blank transaction signature, requested commitment, and complete transaction outcome. Construction SHALL reject null or blank signatures, null commitments, null outcomes, and `SUCCEEDED` outcomes. `FAILED` SHALL identify a definitive on-chain failure including its slot and compact Solana error, while `TIMED_OUT` SHALL identify an unknown outcome and explicitly warn against automatic equivalent resubmission.
|
||||||
|
|
||||||
|
#### Scenario: Confirmed on-chain failure
|
||||||
|
- **WHEN** an increase or decrease signature reaches `CONFIRMED` with a `FAILED` outcome
|
||||||
|
- **THEN** the service throws the checked outcome exception preserving the signature, `CONFIRMED`, slot, and complete compact Solana failure details
|
||||||
|
|
||||||
|
#### Scenario: Confirmation timeout
|
||||||
|
- **WHEN** an increase or decrease signature produces a `TIMED_OUT` outcome
|
||||||
|
- **THEN** the service throws the checked outcome exception preserving the signature, `CONFIRMED`, and unknown outcome without automatically resubmitting
|
||||||
|
|
||||||
|
#### Scenario: Successful outcome is supplied to exception
|
||||||
|
- **WHEN** a caller attempts to construct the outcome exception with `SUCCEEDED` or inconsistent null or blank data
|
||||||
|
- **THEN** construction fails immediately
|
||||||
|
|
||||||
|
### Requirement: Perps confirmation timeout policy
|
||||||
|
The reference service SHALL accept a constructor-injected, non-null, strictly positive confirmation timeout and SHALL preserve its existing constructor with a two-minute default. The configured duration SHALL be forwarded unchanged and SHALL begin governing only when awaiting the already submitted signature starts. Increase and decrease SHALL apply the same confirmation and outcome policy.
|
||||||
|
|
||||||
|
#### Scenario: Existing constructor is used
|
||||||
|
- **WHEN** the reference service is constructed without a confirmation timeout
|
||||||
|
- **THEN** each submitted increase or decrease uses a two-minute confirmation timeout
|
||||||
|
|
||||||
|
#### Scenario: Custom timeout is used
|
||||||
|
- **WHEN** the reference service is constructed with a positive duration
|
||||||
|
- **THEN** each submitted increase or decrease forwards exactly that duration to `CONFIRMED` awaiting
|
||||||
|
|
||||||
|
#### Scenario: Invalid timeout is supplied
|
||||||
|
- **WHEN** the reference service is constructed with a null, zero, or negative duration
|
||||||
|
- **THEN** construction fails before any Perps operation can be submitted
|
||||||
|
|
||||||
|
### Requirement: Post-submission communication and interruption policy
|
||||||
|
If independent confirmation throws an I/O error after a signature is known, the service SHALL throw an `IOException` whose message identifies the increase or decrease operation, includes the signature, and states that the on-chain outcome is unknown, retaining the original error as cause. `InterruptedException` SHALL propagate directly without another RPC or Jupiter request. Confirmation SHALL never submit, rebuild, re-sign, retry, or resubmit a transaction.
|
||||||
|
|
||||||
|
#### Scenario: Confirmation communication fails
|
||||||
|
- **WHEN** independent confirmation throws an I/O error for a known Perps signature
|
||||||
|
- **THEN** the service reports the operation, signature, and unknown outcome, preserves the original error, and performs no retry or resubmission
|
||||||
|
|
||||||
|
#### Scenario: Confirmation is interrupted
|
||||||
|
- **WHEN** independent confirmation throws `InterruptedException`
|
||||||
|
- **THEN** interruption propagates directly and no subsequent request occurs
|
||||||
|
|
||||||
|
### Requirement: Outcome-aware Perps alarm reporting
|
||||||
|
Perps increase and decrease alarm actions SHALL rely on the Perps service for confirmation and SHALL distinguish structured `FAILED`, structured `TIMED_OUT`, and other errors. A failed report SHALL include operation, signature, confirmation slot, and compact Solana failure details. A timed-out report SHALL include operation, signature, unknown-outcome status, and that no equivalent transaction was automatically resubmitted. Alarm actions SHALL NOT invoke transaction awaiting themselves.
|
||||||
|
|
||||||
|
#### Scenario: Alarm operation fails on-chain
|
||||||
|
- **WHEN** a Perps alarm action receives a structured `FAILED` outcome exception
|
||||||
|
- **THEN** it reports the operation, signature, confirmation slot, and compact on-chain failure details distinctly from generic errors
|
||||||
|
|
||||||
|
#### Scenario: Alarm operation remains unknown
|
||||||
|
- **WHEN** a Perps alarm action receives a structured `TIMED_OUT` outcome exception
|
||||||
|
- **THEN** it reports the operation, signature, unknown outcome, and absence of automatic equivalent resubmission distinctly from generic errors
|
||||||
@@ -78,15 +78,45 @@ After successful signing, the service SHALL reject a blank signed transaction an
|
|||||||
- **THEN** the service fails without submitting an execution request
|
- **THEN** the service fails without submitting an execution request
|
||||||
|
|
||||||
### Requirement: Confirmed execution result validation
|
### Requirement: Confirmed execution result validation
|
||||||
The service SHALL return success only when the execution response has status `Success`, explicitly has result code `0`, contains a non-blank transaction signature, and contains positive valid actual total input and output raw amounts. It SHALL convert each actual amount using the independently resolved precision of its mint. Non-success HTTP responses, malformed responses, expired or rejected swaps, failed or contradictory status/code combinations, missing result codes, blank signatures, and invalid result amounts SHALL be reported as failures.
|
The service SHALL first accept a Jupiter execution response only when it has status `Success`, explicitly has result code `0`, contains a non-blank transaction signature, and contains positive valid actual total input and output raw amounts. It SHALL convert each actual amount using the independently resolved precision of its mint. After this provider-response validation, the service SHALL independently await the returned signature on Solana at `CONFIRMED` using its configured confirmation timeout and SHALL return the already validated swap result only for a `SUCCEEDED` outcome. Non-success HTTP responses, malformed responses, expired or rejected swaps, failed or contradictory status/code combinations, missing result codes, blank signatures, and invalid result amounts SHALL remain provider failures and SHALL not be represented as successful swaps.
|
||||||
|
|
||||||
#### Scenario: Successful execution response
|
#### Scenario: Provider response and on-chain confirmation succeed
|
||||||
- **WHEN** Jupiter reports status `Success`, code `0`, a signature, and valid actual total input and output amounts
|
- **WHEN** Jupiter reports status `Success`, code `0`, a signature, and valid actual total input and output amounts, and that signature independently reaches `CONFIRMED` with a `SUCCEEDED` outcome
|
||||||
- **THEN** the service returns those actual amounts in human-readable units and the reported transaction signature
|
- **THEN** the service returns the signature and those actual amounts in human-readable units
|
||||||
|
|
||||||
#### Scenario: Jupiter rejects or fails execution
|
#### Scenario: Jupiter rejects or fails execution
|
||||||
- **WHEN** Jupiter returns a failed status, a missing or non-zero result code, a contradictory status/code combination, expiration, rejection, non-success HTTP status, malformed body, blank signature, or invalid actual amount
|
- **WHEN** Jupiter returns a failed status, a missing or non-zero result code, a contradictory status/code combination, expiration, rejection, non-success HTTP status, malformed body, blank signature, or invalid actual amount
|
||||||
- **THEN** the service reports an I/O failure and does not represent the swap as successful
|
- **THEN** the service reports an I/O failure without treating provider validation as an on-chain confirmation
|
||||||
|
|
||||||
|
#### Scenario: Provider succeeds but transaction fails on-chain
|
||||||
|
- **WHEN** a fully validated Jupiter execution response contains a signature whose independent `CONFIRMED` outcome is `FAILED`
|
||||||
|
- **THEN** the service throws a structured checked outcome exception preserving the signature, requested commitment, slot, and complete compact Solana failure details
|
||||||
|
|
||||||
|
#### Scenario: Provider succeeds but confirmation times out
|
||||||
|
- **WHEN** a fully validated Jupiter execution response contains a signature whose independent `CONFIRMED` outcome is `TIMED_OUT`
|
||||||
|
- **THEN** the service throws a structured checked outcome exception preserving the signature, requested commitment, and unknown outcome warning and does not automatically resubmit an equivalent transaction
|
||||||
|
|
||||||
|
### Requirement: Swap confirmation timeout policy
|
||||||
|
The reference service SHALL accept a constructor-injected, non-null, strictly positive confirmation timeout and SHALL preserve its existing constructor with a two-minute default. The configured duration SHALL be passed unchanged to independent confirmation after submission and SHALL begin governing only when awaiting the known signature starts.
|
||||||
|
|
||||||
|
#### Scenario: Valid custom timeout
|
||||||
|
- **WHEN** a reference service is constructed with a positive duration and later obtains a validated execution signature
|
||||||
|
- **THEN** it awaits that signature at `CONFIRMED` with exactly the supplied duration
|
||||||
|
|
||||||
|
#### Scenario: Invalid custom timeout
|
||||||
|
- **WHEN** a reference service is constructed with a null, zero, or negative duration
|
||||||
|
- **THEN** construction fails before any operation can be submitted
|
||||||
|
|
||||||
|
### Requirement: Post-submission communication and interruption policy
|
||||||
|
If independent confirmation fails with an I/O error after the signature is known, the service SHALL throw an `IOException` whose message identifies the swap operation, includes the signature, and states that the on-chain outcome is unknown, with the original error retained as its cause. Interruption SHALL propagate directly as `InterruptedException` without another RPC or Jupiter request. Neither condition SHALL trigger rebuilding, re-signing, retrying, or resubmitting a transaction.
|
||||||
|
|
||||||
|
#### Scenario: Confirmation communication fails
|
||||||
|
- **WHEN** independent confirmation throws an I/O error for a known submitted signature
|
||||||
|
- **THEN** the service reports the signature and unknown on-chain outcome while preserving the original error and performs no retry or resubmission
|
||||||
|
|
||||||
|
#### Scenario: Confirmation is interrupted
|
||||||
|
- **WHEN** independent confirmation throws `InterruptedException`
|
||||||
|
- **THEN** that interruption propagates directly and the service performs no subsequent request
|
||||||
|
|
||||||
### Requirement: API and implementation separation
|
### Requirement: API and implementation separation
|
||||||
The stable service interface and result value SHALL reside in the Jupiter swap API package, while HTTP DTOs, endpoint handling, throttling, and the reference implementation SHALL remain in the reference-implementation package.
|
The stable service interface and result value SHALL reside in the Jupiter swap API package, while HTTP DTOs, endpoint handling, throttling, and the reference implementation SHALL remain in the reference-implementation package.
|
||||||
|
|||||||
+34
@@ -1,6 +1,7 @@
|
|||||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||||
|
|
||||||
import com.r35157.cryptowallet.solana.SolanaWallet;
|
import com.r35157.cryptowallet.solana.SolanaWallet;
|
||||||
|
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
|
||||||
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
|
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
|
||||||
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
|
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
|
||||||
|
|
||||||
@@ -66,11 +67,44 @@ public final class JupiterPerpsPositionDecreaseAlarmAction
|
|||||||
System.out.println(
|
System.out.println(
|
||||||
"Position Decrease Signature: " + signature
|
"Position Decrease Signature: " + signature
|
||||||
);
|
);
|
||||||
|
} catch (JupiterTransactionOutcomeException e) {
|
||||||
|
reportTransactionOutcome(e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
System.err.println(
|
||||||
|
"Jupiter Perps position decrease was interrupted"
|
||||||
|
);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
System.err.println("EXCEPTION: " + e.getMessage());
|
System.err.println("EXCEPTION: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void reportTransactionOutcome(
|
||||||
|
JupiterTransactionOutcomeException exception
|
||||||
|
) {
|
||||||
|
switch (exception.outcome().status()) {
|
||||||
|
case FAILED -> System.err.println(
|
||||||
|
"Jupiter Perps position decrease failed on-chain: "
|
||||||
|
+ "transaction "
|
||||||
|
+ exception.transactionSignature()
|
||||||
|
+ ", confirmation slot "
|
||||||
|
+ exception.outcome().slot()
|
||||||
|
+ ", Solana error "
|
||||||
|
+ exception.outcome().failureDetails()
|
||||||
|
);
|
||||||
|
case TIMED_OUT -> System.err.println(
|
||||||
|
"Jupiter Perps position decrease transaction "
|
||||||
|
+ exception.transactionSignature()
|
||||||
|
+ " has an unknown on-chain outcome after timeout; "
|
||||||
|
+ "no equivalent transaction was automatically "
|
||||||
|
+ "resubmitted"
|
||||||
|
);
|
||||||
|
case SUCCEEDED -> throw new IllegalStateException(
|
||||||
|
"A successful transaction outcome is not exceptional"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private JupiterPerpsPosition findOpenPosition(
|
private JupiterPerpsPosition findOpenPosition(
|
||||||
JupiterPerpsPositionDecreaseAlarmActionConfiguration
|
JupiterPerpsPositionDecreaseAlarmActionConfiguration
|
||||||
.ResolvedPositionDecrease positionDecrease
|
.ResolvedPositionDecrease positionDecrease
|
||||||
|
|||||||
+34
@@ -1,6 +1,7 @@
|
|||||||
package com.r35157.jupiterperpsalarm.impl.ref;
|
package com.r35157.jupiterperpsalarm.impl.ref;
|
||||||
|
|
||||||
import com.r35157.cryptowallet.solana.SolanaWallet;
|
import com.r35157.cryptowallet.solana.SolanaWallet;
|
||||||
|
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
|
||||||
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
|
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
|
||||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||||
|
|
||||||
@@ -57,11 +58,44 @@ public final class JupiterPerpsPositionIncreaseAlarmAction implements Configured
|
|||||||
);
|
);
|
||||||
|
|
||||||
System.out.println("Position Increase Signature: " + signature);
|
System.out.println("Position Increase Signature: " + signature);
|
||||||
|
} catch (JupiterTransactionOutcomeException e) {
|
||||||
|
reportTransactionOutcome(e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
System.err.println(
|
||||||
|
"Jupiter Perps position increase was interrupted"
|
||||||
|
);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
System.err.println("EXCEPTION: " + e.getMessage());
|
System.err.println("EXCEPTION: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void reportTransactionOutcome(
|
||||||
|
JupiterTransactionOutcomeException exception
|
||||||
|
) {
|
||||||
|
switch (exception.outcome().status()) {
|
||||||
|
case FAILED -> System.err.println(
|
||||||
|
"Jupiter Perps position increase failed on-chain: "
|
||||||
|
+ "transaction "
|
||||||
|
+ exception.transactionSignature()
|
||||||
|
+ ", confirmation slot "
|
||||||
|
+ exception.outcome().slot()
|
||||||
|
+ ", Solana error "
|
||||||
|
+ exception.outcome().failureDetails()
|
||||||
|
);
|
||||||
|
case TIMED_OUT -> System.err.println(
|
||||||
|
"Jupiter Perps position increase transaction "
|
||||||
|
+ exception.transactionSignature()
|
||||||
|
+ " has an unknown on-chain outcome after timeout; "
|
||||||
|
+ "no equivalent transaction was automatically "
|
||||||
|
+ "resubmitted"
|
||||||
|
);
|
||||||
|
case SUCCEEDED -> throw new IllegalStateException(
|
||||||
|
"A successful transaction outcome is not exceptional"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isUSDCBalanceOk(JupiterPerpsPositionIncreaseAlarmActionConfiguration.ResolvedPositionIncrease positionIncrease)
|
private boolean isUSDCBalanceOk(JupiterPerpsPositionIncreaseAlarmActionConfiguration.ResolvedPositionIncrease positionIncrease)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
ΩAmountΩ usdcBalance = wallet.getSPLTokenBalance(
|
ΩAmountΩ usdcBalance = wallet.getSPLTokenBalance(
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package com.r35157.libs.jupiter;
|
||||||
|
|
||||||
|
import com.r35157.libs.solana.SolanaCommitment;
|
||||||
|
import com.r35157.libs.solana.SolanaTransactionOutcome;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports a definitive on-chain failure or an unknown timeout for a Jupiter
|
||||||
|
* transaction whose signature is already known.
|
||||||
|
*
|
||||||
|
* <p>This checked exception never represents a successful transaction. A
|
||||||
|
* timed-out outcome is unknown and must not cause an equivalent transaction
|
||||||
|
* to be automatically resubmitted.</p>
|
||||||
|
*/
|
||||||
|
public final class JupiterTransactionOutcomeException extends Exception {
|
||||||
|
/**
|
||||||
|
* Creates an exception for a failed or timed-out Jupiter transaction.
|
||||||
|
*
|
||||||
|
* @param transactionSignature known submitted transaction signature
|
||||||
|
* @param requestedCommitment commitment requested from Solana
|
||||||
|
* @param outcome complete failed or timed-out transaction outcome
|
||||||
|
* @throws IllegalArgumentException if the signature is blank or the
|
||||||
|
* outcome is successful
|
||||||
|
* @throws NullPointerException if any argument is {@code null}
|
||||||
|
*/
|
||||||
|
public JupiterTransactionOutcomeException(
|
||||||
|
@NotNull ΩSolanaTransactionSignatureΩ transactionSignature,
|
||||||
|
@NotNull SolanaCommitment requestedCommitment,
|
||||||
|
@NotNull SolanaTransactionOutcome outcome
|
||||||
|
) {
|
||||||
|
super(buildMessage(
|
||||||
|
requireSignature(transactionSignature),
|
||||||
|
Objects.requireNonNull(
|
||||||
|
requestedCommitment,
|
||||||
|
"requestedCommitment"
|
||||||
|
),
|
||||||
|
requireExceptionalOutcome(outcome)
|
||||||
|
));
|
||||||
|
this.transactionSignature = transactionSignature;
|
||||||
|
this.requestedCommitment = requestedCommitment;
|
||||||
|
this.outcome = outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the known submitted transaction signature.
|
||||||
|
*
|
||||||
|
* @return non-blank Solana transaction signature
|
||||||
|
*/
|
||||||
|
public @NotNull ΩSolanaTransactionSignatureΩ transactionSignature() {
|
||||||
|
return transactionSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the commitment that the Jupiter operation required.
|
||||||
|
*
|
||||||
|
* @return requested Solana commitment
|
||||||
|
*/
|
||||||
|
public @NotNull SolanaCommitment requestedCommitment() {
|
||||||
|
return requestedCommitment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the complete failed or timed-out outcome.
|
||||||
|
*
|
||||||
|
* @return complete exceptional Solana transaction outcome
|
||||||
|
*/
|
||||||
|
public @NotNull SolanaTransactionOutcome outcome() {
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ΩSolanaTransactionSignatureΩ requireSignature(
|
||||||
|
ΩSolanaTransactionSignatureΩ transactionSignature
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(
|
||||||
|
transactionSignature,
|
||||||
|
"transactionSignature"
|
||||||
|
);
|
||||||
|
if (transactionSignature.isBlank()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Transaction signature must not be blank"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return transactionSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SolanaTransactionOutcome requireExceptionalOutcome(
|
||||||
|
SolanaTransactionOutcome outcome
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(outcome, "outcome");
|
||||||
|
if (outcome.status() == SolanaTransactionOutcome.Status.SUCCEEDED) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"A successful transaction outcome is not exceptional"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildMessage(
|
||||||
|
ΩSolanaTransactionSignatureΩ transactionSignature,
|
||||||
|
SolanaCommitment requestedCommitment,
|
||||||
|
SolanaTransactionOutcome outcome
|
||||||
|
) {
|
||||||
|
return switch (outcome.status()) {
|
||||||
|
case FAILED -> "Jupiter transaction "
|
||||||
|
+ transactionSignature
|
||||||
|
+ " definitively failed on-chain at "
|
||||||
|
+ requestedCommitment
|
||||||
|
+ " in slot "
|
||||||
|
+ outcome.slot()
|
||||||
|
+ ": "
|
||||||
|
+ outcome.failureDetails();
|
||||||
|
case TIMED_OUT -> "Jupiter transaction "
|
||||||
|
+ transactionSignature
|
||||||
|
+ " did not reach a definitive "
|
||||||
|
+ requestedCommitment
|
||||||
|
+ " outcome before timeout; the on-chain outcome is "
|
||||||
|
+ "unknown and an equivalent transaction must not be "
|
||||||
|
+ "automatically resubmitted";
|
||||||
|
case SUCCEEDED -> throw new IllegalArgumentException(
|
||||||
|
"A successful transaction outcome is not exceptional"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ΩSolanaTransactionSignatureΩ transactionSignature;
|
||||||
|
private final SolanaCommitment requestedCommitment;
|
||||||
|
private final SolanaTransactionOutcome outcome;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.r35157.libs.jupiter.perps;
|
package com.r35157.libs.jupiter.perps;
|
||||||
|
|
||||||
|
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ import java.util.Set;
|
|||||||
* operations for its configured wallet.
|
* operations for its configured wallet.
|
||||||
*
|
*
|
||||||
* <p>The service constructs and submits Jupiter transactions, while its
|
* <p>The service constructs and submits Jupiter transactions, while its
|
||||||
* configured wallet is responsible for signing them.</p>
|
* configured wallet is responsible for signing them. Position operations
|
||||||
|
* return normally only after independent Solana {@code CONFIRMED} success.</p>
|
||||||
*/
|
*/
|
||||||
public interface JupiterPerpsService {
|
public interface JupiterPerpsService {
|
||||||
/**
|
/**
|
||||||
@@ -49,17 +51,26 @@ public interface JupiterPerpsService {
|
|||||||
* Opens or increases a Jupiter Perps position.
|
* Opens or increases a Jupiter Perps position.
|
||||||
*
|
*
|
||||||
* <p>The service constructs the transaction, asks its configured wallet
|
* <p>The service constructs the transaction, asks its configured wallet
|
||||||
* to sign it, and submits it to Jupiter.</p>
|
* to sign it, submits it to Jupiter, and independently awaits Solana
|
||||||
|
* {@code CONFIRMED} success. A timeout or communication error after
|
||||||
|
* submission leaves the on-chain outcome unknown and must not cause an
|
||||||
|
* automatic resubmission.</p>
|
||||||
*
|
*
|
||||||
* @param tradedTokenMint the mint address of the asset being traded
|
* @param tradedTokenMint the mint address of the asset being traded
|
||||||
* @param direction whether the position is long or short
|
* @param direction whether the position is long or short
|
||||||
* @param inputTokenAmount the amount of USDC to supply as collateral
|
* @param inputTokenAmount the amount of USDC to supply as collateral
|
||||||
* @param sizeUsdDelta the requested increase in position size, denominated in USD
|
* @param sizeUsdDelta the requested increase in position size, denominated in USD
|
||||||
* @param maxSlippageBps the maximum accepted slippage, in basis points
|
* @param maxSlippageBps the maximum accepted slippage, in basis points
|
||||||
* @return the Solana transaction signature
|
* @return the independently confirmed Solana transaction signature
|
||||||
* @throws IllegalArgumentException if an amount, mint, or slippage value is invalid
|
* @throws IllegalArgumentException if an amount, mint, or slippage value is invalid
|
||||||
* @throws IOException if the transaction cannot be constructed, signed, or submitted
|
* @throws IOException if the transaction cannot be constructed, signed,
|
||||||
* @throws InterruptedException if the calling thread is interrupted
|
* submitted, or confirmed; after submission the
|
||||||
|
* on-chain outcome can be unknown
|
||||||
|
* @throws JupiterTransactionOutcomeException if the submitted transaction
|
||||||
|
* definitively fails on-chain or confirmation times out with an
|
||||||
|
* unknown outcome
|
||||||
|
* @throws InterruptedException if the calling thread is interrupted;
|
||||||
|
* interruption propagates directly
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
ΩSolanaTransactionSignatureΩ executePositionIncrease(
|
ΩSolanaTransactionSignatureΩ executePositionIncrease(
|
||||||
@@ -68,22 +79,32 @@ public interface JupiterPerpsService {
|
|||||||
@NotNull ΩUSDCAmountΩ inputTokenAmount,
|
@NotNull ΩUSDCAmountΩ inputTokenAmount,
|
||||||
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
||||||
int maxSlippageBps
|
int maxSlippageBps
|
||||||
) throws IOException, InterruptedException;
|
) throws IOException, JupiterTransactionOutcomeException,
|
||||||
|
InterruptedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decreases an existing Jupiter Perps position.
|
* Decreases an existing Jupiter Perps position.
|
||||||
*
|
*
|
||||||
* <p>The service constructs the transaction, asks its configured wallet
|
* <p>The service constructs the transaction, asks its configured wallet
|
||||||
* to sign it, and submits it to Jupiter.</p>
|
* to sign it, submits it to Jupiter, and independently awaits Solana
|
||||||
|
* {@code CONFIRMED} success. A timeout or communication error after
|
||||||
|
* submission leaves the on-chain outcome unknown and must not cause an
|
||||||
|
* automatic resubmission.</p>
|
||||||
*
|
*
|
||||||
* @param positionAccount the Jupiter Perps position to decrease
|
* @param positionAccount the Jupiter Perps position to decrease
|
||||||
* @param receiveTokenMint the mint address of the token to receive
|
* @param receiveTokenMint the mint address of the token to receive
|
||||||
* @param sizeUsdDelta the requested decrease in position size, denominated in USD
|
* @param sizeUsdDelta the requested decrease in position size, denominated in USD
|
||||||
* @param maxSlippageBps the maximum accepted slippage, in basis points
|
* @param maxSlippageBps the maximum accepted slippage, in basis points
|
||||||
* @return the Solana transaction signature
|
* @return the independently confirmed Solana transaction signature
|
||||||
* @throws IllegalArgumentException if an amount, mint, or slippage value is invalid
|
* @throws IllegalArgumentException if an amount, mint, or slippage value is invalid
|
||||||
* @throws IOException if the transaction cannot be constructed, signed, or submitted
|
* @throws IOException if the transaction cannot be constructed, signed,
|
||||||
* @throws InterruptedException if the calling thread is interrupted
|
* submitted, or confirmed; after submission the
|
||||||
|
* on-chain outcome can be unknown
|
||||||
|
* @throws JupiterTransactionOutcomeException if the submitted transaction
|
||||||
|
* definitively fails on-chain or confirmation times out with an
|
||||||
|
* unknown outcome
|
||||||
|
* @throws InterruptedException if the calling thread is interrupted;
|
||||||
|
* interruption propagates directly
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
ΩSolanaTransactionSignatureΩ executePositionDecrease(
|
ΩSolanaTransactionSignatureΩ executePositionDecrease(
|
||||||
@@ -91,5 +112,6 @@ public interface JupiterPerpsService {
|
|||||||
@NotNull ΩSPLMintAddressΩ receiveTokenMint,
|
@NotNull ΩSPLMintAddressΩ receiveTokenMint,
|
||||||
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
||||||
int maxSlippageBps
|
int maxSlippageBps
|
||||||
) throws IOException, InterruptedException;
|
) throws IOException, JupiterTransactionOutcomeException,
|
||||||
|
InterruptedException;
|
||||||
}
|
}
|
||||||
|
|||||||
+105
-7
@@ -3,6 +3,7 @@ package com.r35157.libs.jupiter.perps.impl.anchoridl;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
|
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
|
||||||
import com.r35157.cryptowallet.solana.SolanaWallet;
|
import com.r35157.cryptowallet.solana.SolanaWallet;
|
||||||
|
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
|
||||||
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
|
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
|
||||||
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
|
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
|
||||||
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
|
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
|
||||||
@@ -15,8 +16,10 @@ import com.r35157.libs.jupiter.perps.protocol.IncreasePositionResponse;
|
|||||||
import com.r35157.libs.jupiter.perps.protocol.TransactionMetadata;
|
import com.r35157.libs.jupiter.perps.protocol.TransactionMetadata;
|
||||||
import com.r35157.libs.solana.SolanaAccountInfo;
|
import com.r35157.libs.solana.SolanaAccountInfo;
|
||||||
import com.r35157.libs.solana.SolanaBlockChain;
|
import com.r35157.libs.solana.SolanaBlockChain;
|
||||||
|
import com.r35157.libs.solana.SolanaCommitment;
|
||||||
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
|
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
|
||||||
import com.r35157.libs.solana.SolanaSignedTransaction;
|
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||||
|
import com.r35157.libs.solana.SolanaTransactionOutcome;
|
||||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -30,6 +33,7 @@ import java.net.URI;
|
|||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -39,10 +43,45 @@ import static java.math.BigDecimal.ZERO;
|
|||||||
|
|
||||||
public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Jupiter Perps service with a two-minute transaction
|
||||||
|
* confirmation timeout.
|
||||||
|
*
|
||||||
|
* @param solanaBlockChain blockchain used for account access and
|
||||||
|
* transaction confirmation
|
||||||
|
* @param wallet wallet used to sign Perps transactions
|
||||||
|
* @param currencyIdentityService canonical currency identity service
|
||||||
|
*/
|
||||||
public AnchorIdlJupiterPerpsServiceImpl(
|
public AnchorIdlJupiterPerpsServiceImpl(
|
||||||
SolanaBlockChain solanaBlockChain,
|
@NotNull SolanaBlockChain solanaBlockChain,
|
||||||
SolanaWallet wallet,
|
@NotNull SolanaWallet wallet,
|
||||||
CurrencyIdentityService currencyIdentityService
|
@NotNull CurrencyIdentityService currencyIdentityService
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
solanaBlockChain,
|
||||||
|
wallet,
|
||||||
|
currencyIdentityService,
|
||||||
|
DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Jupiter Perps service with an explicit confirmation timeout.
|
||||||
|
*
|
||||||
|
* @param solanaBlockChain blockchain used for account access and
|
||||||
|
* transaction confirmation
|
||||||
|
* @param wallet wallet used to sign Perps transactions
|
||||||
|
* @param currencyIdentityService canonical currency identity service
|
||||||
|
* @param transactionConfirmationTimeout positive timeout applied when
|
||||||
|
* independently awaiting submitted transaction signatures
|
||||||
|
* @throws NullPointerException if an argument is {@code null}
|
||||||
|
* @throws IllegalArgumentException if the timeout is zero or negative
|
||||||
|
*/
|
||||||
|
public AnchorIdlJupiterPerpsServiceImpl(
|
||||||
|
@NotNull SolanaBlockChain solanaBlockChain,
|
||||||
|
@NotNull SolanaWallet wallet,
|
||||||
|
@NotNull CurrencyIdentityService currencyIdentityService,
|
||||||
|
@NotNull Duration transactionConfirmationTimeout
|
||||||
) {
|
) {
|
||||||
this.solanaBlockChain = Objects.requireNonNull(
|
this.solanaBlockChain = Objects.requireNonNull(
|
||||||
solanaBlockChain,
|
solanaBlockChain,
|
||||||
@@ -53,6 +92,9 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
|||||||
currencyIdentityService,
|
currencyIdentityService,
|
||||||
"currencyIdentityService"
|
"currencyIdentityService"
|
||||||
);
|
);
|
||||||
|
this.transactionConfirmationTimeout = requirePositiveTimeout(
|
||||||
|
transactionConfirmationTimeout
|
||||||
|
);
|
||||||
this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder();
|
this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder();
|
||||||
this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
|
this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
|
||||||
}
|
}
|
||||||
@@ -133,7 +175,8 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
|||||||
@NotNull ΩUSDCAmountΩ inputTokenAmount,
|
@NotNull ΩUSDCAmountΩ inputTokenAmount,
|
||||||
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
||||||
int maxSlippageBps
|
int maxSlippageBps
|
||||||
) throws IOException, InterruptedException {
|
) throws IOException, JupiterTransactionOutcomeException,
|
||||||
|
InterruptedException {
|
||||||
SolanaUnsignedTransaction unsignedTransaction =
|
SolanaUnsignedTransaction unsignedTransaction =
|
||||||
buildPositionIncreaseTransaction(
|
buildPositionIncreaseTransaction(
|
||||||
wallet.getAddress(),
|
wallet.getAddress(),
|
||||||
@@ -147,7 +190,10 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
|||||||
SolanaSignedTransaction signedTransaction =
|
SolanaSignedTransaction signedTransaction =
|
||||||
wallet.signTransaction(unsignedTransaction);
|
wallet.signTransaction(unsignedTransaction);
|
||||||
|
|
||||||
return executePositionIncreaseTransaction(signedTransaction);
|
ΩSolanaTransactionSignatureΩ transactionSignature =
|
||||||
|
executePositionIncreaseTransaction(signedTransaction);
|
||||||
|
awaitConfirmed("Jupiter Perps position increase", transactionSignature);
|
||||||
|
return transactionSignature;
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull SolanaUnsignedTransaction buildPositionIncreaseTransaction(
|
private @NotNull SolanaUnsignedTransaction buildPositionIncreaseTransaction(
|
||||||
@@ -236,7 +282,8 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
|||||||
@NotNull ΩSPLMintAddressΩ receiveTokenMint,
|
@NotNull ΩSPLMintAddressΩ receiveTokenMint,
|
||||||
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
||||||
int maxSlippageBps
|
int maxSlippageBps
|
||||||
) throws IOException, InterruptedException {
|
) throws IOException, JupiterTransactionOutcomeException,
|
||||||
|
InterruptedException {
|
||||||
SolanaUnsignedTransaction unsignedTransaction =
|
SolanaUnsignedTransaction unsignedTransaction =
|
||||||
buildPositionDecreaseTransaction(
|
buildPositionDecreaseTransaction(
|
||||||
positionAccount,
|
positionAccount,
|
||||||
@@ -248,7 +295,55 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
|||||||
SolanaSignedTransaction signedTransaction =
|
SolanaSignedTransaction signedTransaction =
|
||||||
wallet.signTransaction(unsignedTransaction);
|
wallet.signTransaction(unsignedTransaction);
|
||||||
|
|
||||||
return executePositionDecreaseTransaction(signedTransaction);
|
ΩSolanaTransactionSignatureΩ transactionSignature =
|
||||||
|
executePositionDecreaseTransaction(signedTransaction);
|
||||||
|
awaitConfirmed("Jupiter Perps position decrease", transactionSignature);
|
||||||
|
return transactionSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitConfirmed(
|
||||||
|
String operation,
|
||||||
|
ΩSolanaTransactionSignatureΩ transactionSignature
|
||||||
|
) throws IOException, JupiterTransactionOutcomeException,
|
||||||
|
InterruptedException {
|
||||||
|
SolanaTransactionOutcome outcome;
|
||||||
|
try {
|
||||||
|
outcome = solanaBlockChain.awaitTransaction(
|
||||||
|
transactionSignature,
|
||||||
|
SolanaCommitment.CONFIRMED,
|
||||||
|
transactionConfirmationTimeout
|
||||||
|
);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOException(
|
||||||
|
operation
|
||||||
|
+ " confirmation failed for transaction "
|
||||||
|
+ transactionSignature
|
||||||
|
+ "; the on-chain outcome is unknown",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (outcome.status()) {
|
||||||
|
case SUCCEEDED -> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case FAILED, TIMED_OUT -> throw
|
||||||
|
new JupiterTransactionOutcomeException(
|
||||||
|
transactionSignature,
|
||||||
|
SolanaCommitment.CONFIRMED,
|
||||||
|
outcome
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Duration requirePositiveTimeout(Duration timeout) {
|
||||||
|
Objects.requireNonNull(timeout, "transactionConfirmationTimeout");
|
||||||
|
if (timeout.isZero() || timeout.isNegative()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Transaction confirmation timeout must be greater than zero"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull SolanaUnsignedTransaction buildPositionDecreaseTransaction(
|
private @NotNull SolanaUnsignedTransaction buildPositionDecreaseTransaction(
|
||||||
@@ -806,6 +901,8 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
|||||||
private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID = "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu";
|
private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID = "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu";
|
||||||
|
|
||||||
private static final URI INCREASE_POSITION_ENDPOINT = URI.create("https://perps-api.jup.ag/v2/positions/increase");
|
private static final URI INCREASE_POSITION_ENDPOINT = URI.create("https://perps-api.jup.ag/v2/positions/increase");
|
||||||
|
private static final Duration DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT =
|
||||||
|
Duration.ofMinutes(2);
|
||||||
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
|
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
private static final long RATE_POWER = 1_000_000_000L;
|
private static final long RATE_POWER = 1_000_000_000L;
|
||||||
@@ -817,6 +914,7 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
|||||||
private final SolanaBlockChain solanaBlockChain;
|
private final SolanaBlockChain solanaBlockChain;
|
||||||
private final SolanaWallet wallet;
|
private final SolanaWallet wallet;
|
||||||
private final CurrencyIdentityService currencyIdentityService;
|
private final CurrencyIdentityService currencyIdentityService;
|
||||||
|
private final Duration transactionConfirmationTimeout;
|
||||||
private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder;
|
private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder;
|
||||||
private final AnchorIdlJupiterPerpsCustodyDecoder custodyDecoder;
|
private final AnchorIdlJupiterPerpsCustodyDecoder custodyDecoder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.r35157.libs.jupiter.swap;
|
package com.r35157.libs.jupiter.swap;
|
||||||
|
|
||||||
|
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
|
||||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@@ -11,18 +12,19 @@ import java.math.BigDecimal;
|
|||||||
*
|
*
|
||||||
* <p>The service resolves token precision from Solana, obtains and validates a
|
* <p>The service resolves token precision from Solana, obtains and validates a
|
||||||
* Jupiter Swap V2 order, asks its configured wallet to sign the transaction,
|
* Jupiter Swap V2 order, asks its configured wallet to sign the transaction,
|
||||||
* and submits the signed transaction through Jupiter's managed execution
|
* submits the signed transaction through Jupiter's managed execution
|
||||||
* endpoint.</p>
|
* endpoint, and independently awaits Solana confirmation.</p>
|
||||||
*/
|
*/
|
||||||
public interface JupiterSwapService {
|
public interface JupiterSwapService {
|
||||||
/**
|
/**
|
||||||
* Swaps an exact amount of one SPL token for another SPL token.
|
* Swaps an exact amount of one SPL token for another SPL token.
|
||||||
*
|
*
|
||||||
* <p>The returned amounts are the actual wallet-level amounts reported by
|
* <p>The returned amounts are the actual wallet-level amounts reported by
|
||||||
* Jupiter after successful execution, not the quoted amounts. If an error
|
* Jupiter after successful provider execution, not the quoted amounts.
|
||||||
* or interruption occurs after execution submission, the transaction's
|
* Normal return additionally means the reported signature independently
|
||||||
* outcome can be unknown; callers must reload wallet balances before
|
* reached Solana {@code CONFIRMED} success. A timed-out outcome or an I/O
|
||||||
* deciding whether to initiate another swap.</p>
|
* error after submission can leave the transaction outcome unknown and
|
||||||
|
* must never cause automatic resubmission.</p>
|
||||||
*
|
*
|
||||||
* @param inputTokenMint input SPL-token mint address
|
* @param inputTokenMint input SPL-token mint address
|
||||||
* @param inputTokenAmount exact input amount in human-readable token units
|
* @param inputTokenAmount exact input amount in human-readable token units
|
||||||
@@ -37,9 +39,14 @@ public interface JupiterSwapService {
|
|||||||
* unsupported token program, or has invalid
|
* unsupported token program, or has invalid
|
||||||
* metadata
|
* metadata
|
||||||
* @throws IOException if Solana or Jupiter communication, response
|
* @throws IOException if Solana or Jupiter communication, response
|
||||||
* decoding, signing, or execution fails
|
* decoding, signing, execution, or confirmation fails;
|
||||||
|
* after submission the on-chain outcome can be unknown
|
||||||
|
* @throws JupiterTransactionOutcomeException if the submitted transaction
|
||||||
|
* definitively fails on-chain or confirmation times out with an
|
||||||
|
* unknown outcome
|
||||||
* @throws InterruptedException if order pacing, Solana access, signing, or
|
* @throws InterruptedException if order pacing, Solana access, signing, or
|
||||||
* Jupiter communication is interrupted
|
* Jupiter communication or confirmation is
|
||||||
|
* interrupted; interruption propagates directly
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
JupiterSwapResult swap(
|
JupiterSwapResult swap(
|
||||||
@@ -47,5 +54,6 @@ public interface JupiterSwapService {
|
|||||||
@NotNull ΩAmountΩ inputTokenAmount,
|
@NotNull ΩAmountΩ inputTokenAmount,
|
||||||
@NotNull ΩSPLMintAddressΩ outputTokenMint,
|
@NotNull ΩSPLMintAddressΩ outputTokenMint,
|
||||||
int maxSlippageBps
|
int maxSlippageBps
|
||||||
) throws IOException, InterruptedException;
|
) throws IOException, JupiterTransactionOutcomeException,
|
||||||
|
InterruptedException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
|||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.r35157.cryptowallet.solana.SolanaWallet;
|
import com.r35157.cryptowallet.solana.SolanaWallet;
|
||||||
|
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
|
||||||
import com.r35157.libs.jupiter.swap.JupiterSwapResult;
|
import com.r35157.libs.jupiter.swap.JupiterSwapResult;
|
||||||
import com.r35157.libs.jupiter.swap.JupiterSwapService;
|
import com.r35157.libs.jupiter.swap.JupiterSwapService;
|
||||||
import com.r35157.libs.solana.SPLTokenSupply;
|
import com.r35157.libs.solana.SPLTokenSupply;
|
||||||
import com.r35157.libs.solana.SolanaAccountInfo;
|
import com.r35157.libs.solana.SolanaAccountInfo;
|
||||||
import com.r35157.libs.solana.SolanaBlockChain;
|
import com.r35157.libs.solana.SolanaBlockChain;
|
||||||
|
import com.r35157.libs.solana.SolanaCommitment;
|
||||||
import com.r35157.libs.solana.SolanaSignedTransaction;
|
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||||
|
import com.r35157.libs.solana.SolanaTransactionOutcome;
|
||||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||||
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
||||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||||
@@ -43,6 +46,29 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
|
|||||||
public JupiterSwapServiceImpl(
|
public JupiterSwapServiceImpl(
|
||||||
@NotNull SolanaBlockChain solanaBlockChain,
|
@NotNull SolanaBlockChain solanaBlockChain,
|
||||||
@NotNull SolanaWallet solanaWallet
|
@NotNull SolanaWallet solanaWallet
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
solanaBlockChain,
|
||||||
|
solanaWallet,
|
||||||
|
DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Jupiter swap service with an explicit confirmation timeout.
|
||||||
|
*
|
||||||
|
* @param solanaBlockChain blockchain access used to resolve mint metadata
|
||||||
|
* and confirm submitted transactions
|
||||||
|
* @param solanaWallet wallet used as taker and transaction signer
|
||||||
|
* @param transactionConfirmationTimeout positive timeout applied when
|
||||||
|
* independently awaiting a submitted transaction signature
|
||||||
|
* @throws NullPointerException if an argument is {@code null}
|
||||||
|
* @throws IllegalArgumentException if the timeout is zero or negative
|
||||||
|
*/
|
||||||
|
public JupiterSwapServiceImpl(
|
||||||
|
@NotNull SolanaBlockChain solanaBlockChain,
|
||||||
|
@NotNull SolanaWallet solanaWallet,
|
||||||
|
@NotNull Duration transactionConfirmationTimeout
|
||||||
) {
|
) {
|
||||||
this.solanaBlockChain = Objects.requireNonNull(
|
this.solanaBlockChain = Objects.requireNonNull(
|
||||||
solanaBlockChain,
|
solanaBlockChain,
|
||||||
@@ -52,6 +78,9 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
|
|||||||
solanaWallet,
|
solanaWallet,
|
||||||
"solanaWallet"
|
"solanaWallet"
|
||||||
);
|
);
|
||||||
|
this.transactionConfirmationTimeout = requirePositiveTimeout(
|
||||||
|
transactionConfirmationTimeout
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -60,7 +89,8 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
|
|||||||
@NotNull ΩAmountΩ inputTokenAmount,
|
@NotNull ΩAmountΩ inputTokenAmount,
|
||||||
@NotNull ΩSPLMintAddressΩ outputTokenMint,
|
@NotNull ΩSPLMintAddressΩ outputTokenMint,
|
||||||
int maxSlippageBps
|
int maxSlippageBps
|
||||||
) throws IOException, InterruptedException {
|
) throws IOException, JupiterTransactionOutcomeException,
|
||||||
|
InterruptedException {
|
||||||
validateArguments(
|
validateArguments(
|
||||||
inputTokenMint,
|
inputTokenMint,
|
||||||
inputTokenAmount,
|
inputTokenAmount,
|
||||||
@@ -116,11 +146,56 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
|
|||||||
validatedOrder.lastValidBlockHeight()
|
validatedOrder.lastValidBlockHeight()
|
||||||
);
|
);
|
||||||
|
|
||||||
return validateExecution(
|
JupiterSwapResult result = validateExecution(
|
||||||
execution,
|
execution,
|
||||||
inputMetadata.decimals(),
|
inputMetadata.decimals(),
|
||||||
outputMetadata.decimals()
|
outputMetadata.decimals()
|
||||||
);
|
);
|
||||||
|
awaitConfirmed(result.transactionSignature());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitConfirmed(
|
||||||
|
ΩSolanaTransactionSignatureΩ transactionSignature
|
||||||
|
) throws IOException, JupiterTransactionOutcomeException,
|
||||||
|
InterruptedException {
|
||||||
|
SolanaTransactionOutcome outcome;
|
||||||
|
try {
|
||||||
|
outcome = solanaBlockChain.awaitTransaction(
|
||||||
|
transactionSignature,
|
||||||
|
SolanaCommitment.CONFIRMED,
|
||||||
|
transactionConfirmationTimeout
|
||||||
|
);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOException(
|
||||||
|
"Jupiter Swap confirmation failed for transaction "
|
||||||
|
+ transactionSignature
|
||||||
|
+ "; the on-chain outcome is unknown",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (outcome.status()) {
|
||||||
|
case SUCCEEDED -> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case FAILED, TIMED_OUT -> throw
|
||||||
|
new JupiterTransactionOutcomeException(
|
||||||
|
transactionSignature,
|
||||||
|
SolanaCommitment.CONFIRMED,
|
||||||
|
outcome
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Duration requirePositiveTimeout(Duration timeout) {
|
||||||
|
Objects.requireNonNull(timeout, "transactionConfirmationTimeout");
|
||||||
|
if (timeout.isZero() || timeout.isNegative()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Transaction confirmation timeout must be greater than zero"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateArguments(
|
private static void validateArguments(
|
||||||
@@ -691,6 +766,8 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
|
|||||||
"https://api.jup.ag/swap/v2/execute"
|
"https://api.jup.ag/swap/v2/execute"
|
||||||
);
|
);
|
||||||
private static final Duration HTTP_TIMEOUT = Duration.ofSeconds(30);
|
private static final Duration HTTP_TIMEOUT = Duration.ofSeconds(30);
|
||||||
|
private static final Duration DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT =
|
||||||
|
Duration.ofMinutes(2);
|
||||||
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
|
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
|
||||||
.connectTimeout(HTTP_TIMEOUT)
|
.connectTimeout(HTTP_TIMEOUT)
|
||||||
.build();
|
.build();
|
||||||
@@ -709,4 +786,5 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
|
|||||||
|
|
||||||
private final SolanaBlockChain solanaBlockChain;
|
private final SolanaBlockChain solanaBlockChain;
|
||||||
private final SolanaWallet solanaWallet;
|
private final SolanaWallet solanaWallet;
|
||||||
|
private final Duration transactionConfirmationTimeout;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user