Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add errors for Solana cases that precede confirmed #2544

Merged
merged 16 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions services/payments/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,26 @@ type InsufficientAuthorizationsError struct{}
func (e *InsufficientAuthorizationsError) Error() string {
return "insufficient authorizations"
}

// SolanaTransactionNotConfirmedError indicates that a Solana transaction is in any known status
pavelbrm marked this conversation as resolved.
Show resolved Hide resolved
// preceding confirmation on the Solana chain.
type SolanaTransactionNotConfirmedError struct{}

func (e *SolanaTransactionNotConfirmedError) Error() string {
return "transaction not confirmed"
}

// SolanaTransactionNotFoundError indicates that a Solana transaction was not found on the chain
type SolanaTransactionNotFoundError struct{}

func (e *SolanaTransactionNotFoundError) Error() string {
return "transaction not found"
}

// SolanaTransactionUnknownError indicates that a response was received but no known status could
// be derived
type SolanaTransactionUnknownError struct{}

func (e *SolanaTransactionUnknownError) Error() string {
return "transaction status unknown"
}
4 changes: 3 additions & 1 deletion services/payments/statemachine.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,12 @@ func (s *Service) DriveTransaction(
state, lastErr := Drive(ctx, stateMachine)
if state != nil {
var errTmp paymentLib.PaymentError
// If the error is already a PaymentError use it
if errors.As(lastErr, &errTmp) {
state.LastError = &errTmp
} else if lastErr != nil {
// Assume any non-categorized error is temporary
// If it's not a PaymentError turn it into one. Assume any error coming to us without
// PaymentError structure is temporary
state.LastError = paymentLib.ProcessingErrorFromError(lastErr, true)
} else {
state.LastError = nil
Expand Down
35 changes: 28 additions & 7 deletions services/payments/statemachine_solana.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,16 +198,33 @@ func (sm *SolanaMachine) Pay(ctx context.Context) (*paymentLib.AuthenticatedPaym
}

b58Signature := base58.Encode(idempotencyData.Transaction.Signatures[0])
// solanaError stores temporary errors that need to be recorded as the LastError in QLDB but
// that are expected and do not need to be acted upon. It will only be returned if no other
// unexpected errors arise from processing.
var solanaError error
status, err := checkStatus(ctx, b58Signature, &sm.solanaRpcClient)
if err != nil {
return sm.transaction, fmt.Errorf("failed to check transaction status: %w", err)
// Some errors are expected and we just want to record them. Check if the status check
// returned such an error and prepare to return it after we attempt to make progress.
var (
errNotConfirmed SolanaTransactionNotConfirmedError
errNotFound SolanaTransactionNotFoundError
errUnknown SolanaTransactionUnknownError
Sneagan marked this conversation as resolved.
Show resolved Hide resolved
)
if errors.As(err, &errNotConfirmed) ||
errors.As(err, errNotFound) ||
errors.As(err, errUnknown) {
Sneagan marked this conversation as resolved.
Show resolved Hide resolved
solanaError = paymentLib.ProcessingErrorFromError(err, true)
} else {
return sm.transaction, fmt.Errorf("failed to check transaction status: %w", err)
}
}
if status == TxnConfirmed || status == TxnFinalized {
entry, err := sm.SetNextState(ctx, paymentLib.Paid)
if err != nil {
return sm.transaction, fmt.Errorf("failed to write next state: %w entry: %v", err, entry)
}
return sm.transaction, nil
return sm.transaction, solanaError
}

// Check if idempotency data has expired before (re)submitting the transaction
Expand Down Expand Up @@ -285,7 +302,7 @@ func (sm *SolanaMachine) Pay(ctx context.Context) (*paymentLib.AuthenticatedPaym
return sm.transaction, fmt.Errorf("failed to write next state: %w entry: %v", err, entry)
}

return sm.transaction, nil
return sm.transaction, solanaError
}

// Fail implements TxStateMachine for the Solana machine.
Expand Down Expand Up @@ -352,17 +369,21 @@ func (sm *SolanaMachine) makeInstructions(
}, nil
}

func checkStatus(ctx context.Context, signature string, client *solanaClient.Client) (TxnCommitmentStatus, error) {
func checkStatus(
ctx context.Context,
signature string,
client *solanaClient.Client,
) (TxnCommitmentStatus, error) {
sigStatus, err := client.GetSignatureStatus(ctx, signature)
if err != nil {
return TxnUnknown, fmt.Errorf("status check error: %w", err)
}

if sigStatus == nil {
return TxnNotFound, nil
return TxnNotFound, &SolanaTransactionNotFoundError{}
pavelbrm marked this conversation as resolved.
Show resolved Hide resolved
}
if sigStatus.ConfirmationStatus == nil {
return TxnUnknown, nil
return TxnUnknown, &SolanaTransactionUnknownError{}
}

if sigStatus.Err != nil {
Expand All @@ -377,7 +398,7 @@ func checkStatus(ctx context.Context, signature string, client *solanaClient.Cli

switch *sigStatus.ConfirmationStatus {
case rpc.CommitmentProcessed:
return TxnProcessed, nil
return TxnProcessed, &SolanaTransactionNotConfirmedError{}
case rpc.CommitmentConfirmed:
return TxnConfirmed, nil
case rpc.CommitmentFinalized:
Expand Down