Skip to content

Commit

Permalink
Add errors for Solana cases that precede confirmed (#2544)
Browse files Browse the repository at this point in the history
Add errors for Solana cases that precede confirmed
---------

Co-authored-by: Pavel Brm <[email protected]>
Co-authored-by: eV <[email protected]>
  • Loading branch information
3 people authored Jun 13, 2024
1 parent 82969fe commit 3152fae
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 50 deletions.
12 changes: 12 additions & 0 deletions services/payments/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,15 @@ type InsufficientAuthorizationsError struct{}
func (e *InsufficientAuthorizationsError) Error() string {
return "insufficient authorizations"
}

const (
ErrSolanaTransactionUnknown Error = "transaction status unknown"
ErrSolanaTransactionNotFound Error = "transaction not found"
ErrSolanaTransactionNotConfirmed Error = "transaction not confirmed"
)

type Error string

func (e Error) Error() string {
return string(e)
}
76 changes: 44 additions & 32 deletions services/payments/live_solana_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math"
"os"
Expand All @@ -19,7 +21,7 @@ import (
"github.com/blocto/solana-go-sdk/client"
"github.com/blocto/solana-go-sdk/common"
"github.com/blocto/solana-go-sdk/rpc"
"github.com/blocto/solana-go-sdk/types"
// "github.com/blocto/solana-go-sdk/types"
appctx "github.com/brave-intl/bat-go/libs/context"
"github.com/brave-intl/bat-go/libs/logging"
paymentLib "github.com/brave-intl/bat-go/libs/payments"
Expand All @@ -30,6 +32,15 @@ import (
must "github.com/stretchr/testify/require"
)

const (
// DEV NET CHAIN INFO
// mint = "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq"
// tokenAccountOwner = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
// MAIN NET CHAIN INFO
mint = "EPeUFDgHRxs9xxEPVaL6kfGQvCon7jmAWKVUHuux1Tpz"
tokenAccountOwner = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
)

/*
* The below tests rely on some environment variables in order to execute:
* SOLANA_SIGNING_KEY
Expand All @@ -42,22 +53,18 @@ TestLiveSolanaStateMachineATAMissing tests for correct state progression from
Initialized to Paid with a payee account that is missing the SPL-BAT ATA.
*/
func TestLiveSolanaStateMachineATAMissing(t *testing.T) {
const (
mint = "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq"
tokenAccountOwner = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
)

ctx, _ := logging.SetupLogger(context.WithValue(context.Background(), appctx.DebugLoggingCTXKey, true))

// New account for every test execution to ensure that the account does
// not already have its ATA configured.
payeeAccount := types.NewAccount()
// not already have its ATA configured. Only used on devnet
//payeeAccount := types.NewAccount()

state := paymentLib.AuthenticatedPaymentState{
Status: paymentLib.Prepared,
PaymentDetails: paymentLib.PaymentDetails{
Amount: decimal.NewFromFloat(1.4),
To: payeeAccount.PublicKey.ToBase58(),
Amount: decimal.NewFromFloat(0.01),
// Hard coded to address that we control for main net testing
To: "4Pyz7vNKfjERMyCwecRhqurLwJWbs2HLKHuxW9QakqR9",
From: os.Getenv("SOLANA_PAYER_ADDRESS"),
Custodian: "solana",
PayoutID: "4b2f22c9-f227-43b3-98d2-4a5337b65bc5",
Expand All @@ -78,7 +85,7 @@ func TestLiveSolanaStateMachineATAMissing(t *testing.T) {
)

createdAta, _, err := common.FindAssociatedTokenAddress(
payeeAccount.PublicKey,
common.PublicKeyFromString(state.PaymentDetails.To),
common.PublicKeyFromString(mint),
)
must.Nil(t, err)
Expand Down Expand Up @@ -110,18 +117,14 @@ TestLiveSolanaStateMachineATAPresent tests for correct state progression from
Initialized to Paid with a payee account that has the SPL-BAT ATA configured.
*/
func TestLiveSolanaStateMachineATAPresent(t *testing.T) {
const (
mint = "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq"
)

ctx, _ := logging.SetupLogger(context.WithValue(context.Background(), appctx.DebugLoggingCTXKey, true))

state := paymentLib.AuthenticatedPaymentState{
Status: paymentLib.Prepared,
PaymentDetails: paymentLib.PaymentDetails{
Amount: decimal.NewFromFloat(1.4),
Amount: decimal.NewFromFloat(0.01),
// Fixed To address that has the ATA configured already
To: "5g7xMFn9bk8vyZdfsr4mAfUWKWDaWxzZBH5Cb1XHftBL",
To: "4Pyz7vNKfjERMyCwecRhqurLwJWbs2HLKHuxW9QakqR9",
From: os.Getenv("SOLANA_PAYER_ADDRESS"),
Custodian: "solana",
PayoutID: "4b2f22c9-f227-43b3-98d2-4a5337b65bc5",
Expand Down Expand Up @@ -151,17 +154,13 @@ func TestLiveSolanaStateMachineATAPresent(t *testing.T) {
}

func TestLiveSolanaStateMachineDropped(t *testing.T) {
const (
mint = "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq"
)

ctx, _ := logging.SetupLogger(context.WithValue(context.Background(), appctx.DebugLoggingCTXKey, true))

state := paymentLib.AuthenticatedPaymentState{
Status: paymentLib.Prepared,
PaymentDetails: paymentLib.PaymentDetails{
Amount: decimal.NewFromFloat(1.4),
To: "5g7xMFn9bk8vyZdfsr4mAfUWKWDaWxzZBH5Cb1XHftBL",
Amount: decimal.NewFromFloat(0.01),
To: "4Pyz7vNKfjERMyCwecRhqurLwJWbs2HLKHuxW9QakqR9",
From: os.Getenv("SOLANA_PAYER_ADDRESS"),
Custodian: "solana",
PayoutID: "4b2f22c9-f227-43b3-98d2-4a5337b65bc5",
Expand Down Expand Up @@ -228,14 +227,19 @@ func checkTransactionMatchesPaymentDetails(
Commitment: rpc.CommitmentConfirmed,
})
must.Nil(t, err)
defer func() {
if err := recover(); err != nil {
t.Logf("panic occurred introspecting transaction:\n%+v\nwith error:\n%s", txn, err)
}
}()
if innerTxn, ok := txn.Result.Transaction.(map[string]interface{}); ok {
if message, ok := innerTxn["message"].(map[string]interface{}); ok {
if instructions, ok := message["instructions"].([]interface{}); ok {
if instructionOne, ok := instructions[2].(map[string]interface{}); ok {
if parsed, ok := instructionOne["parsed"].(map[string]interface{}); ok {
if info, ok := parsed["info"].(map[string]interface{}); ok {
t.Log("Verifying chain transaction mint, to, and from")
should.Equal(t, "AH86ZDiGbV1GSzqtJ6sgfUbXSXrGKKjju4Bs1Gm75AQq", info["mint"])
should.Equal(t, mint, info["mint"])
should.Equal(t, state.PaymentDetails.To, info["wallet"])
should.Equal(t, state.PaymentDetails.From, info["source"])
} else {
Expand Down Expand Up @@ -324,9 +328,9 @@ func driveHappyPathTransitions(
should.Equal(t, idempotencyData, transaction.ExternalIdempotency)
t.Log("State is Pending")

for i := 1; i < 30; i++ {
t.Log("Checking for Paid status")
time.Sleep(100 * time.Millisecond)
for i := 1; i < 65; i++ {
t.Logf("Checking for Paid status (%d/65)\nFound status: %s\nLast error: %v", i, transaction.Status, transaction.LastError)
time.Sleep(1 * time.Second)
md, _ := json.Marshal(transaction)
mockTransitionHistory.Data.UnsafePaymentState = md
solMachine.setTransaction(transaction)
Expand All @@ -336,8 +340,8 @@ func driveHappyPathTransitions(
break
}
}
should.Equal(t, paymentLib.Paid, transaction.Status)
should.Equal(t, idempotencyData, transaction.ExternalIdempotency)
must.Equal(t, paymentLib.Paid, transaction.Status, fmt.Sprintf("%+v", transaction))
must.Equal(t, idempotencyData, transaction.ExternalIdempotency)
t.Log("State is Paid")

return transaction
Expand All @@ -352,16 +356,18 @@ func setupState(
QLDBPaymentTransitionHistoryEntry,
[]byte,
) {
keyBytes, err := base64.StdEncoding.DecodeString(os.Getenv("SOLANA_SIGNING_KEY"))
must.Nil(t, err)
solMachine := SolanaMachine{
signingKey: os.Getenv("SOLANA_SIGNING_KEY"),
signingKey: keyBytes,
solanaRpcClient: *client.NewClient(os.Getenv("SOLANA_RPC_ENDPOINT")),
splMintAddress: mint, // SPL mint address on devnet
splMintDecimals: 8, // SPL mint decimals on devnet
}
idempotencyKey, err := uuid.Parse("1803df27-f29c-537a-9384-bb5b523ea3f7")
must.Nil(t, err)

marshaledData, _ := json.Marshal(state)
marshaledData, err := json.Marshal(state)
must.Nil(t, err)
privkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
must.Nil(t, err)
Expand Down Expand Up @@ -423,6 +429,12 @@ func transition(
mth.Data.UnsafePaymentState = md
sm.setTransaction(&ts)
newTransaction, err := Drive(ctx, &sm)
must.Nil(t, err)
should.True(
t,
err == nil ||
errors.Is(err, SolanaTransactionNotConfirmedError) ||
errors.Is(err, SolanaTransactionNotFoundError) ||
errors.Is(err, SolanaTransactionUnknownError),
)
return newTransaction
}
6 changes: 3 additions & 3 deletions services/payments/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import (

"filippo.io/age"
"filippo.io/age/agessh"
"github.com/brave-intl/bat-go/libs/logging"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/kms"
kmsTypes "github.com/aws/aws-sdk-go-v2/service/kms/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
solTypes "github.com/blocto/solana-go-sdk/types"
appctx "github.com/brave-intl/bat-go/libs/context"
"github.com/brave-intl/bat-go/libs/logging"
"github.com/brave-intl/bat-go/libs/nitro"
nitroawsutils "github.com/brave-intl/bat-go/libs/nitro/aws"
paymentLib "github.com/brave-intl/bat-go/libs/payments"
Expand Down Expand Up @@ -191,7 +191,7 @@ func encryptShares(shares [][]byte, operatorKeys []string) ([]paymentLib.Operato
for i, share := range shares {
recipient, err := agessh.ParseRecipient(operatorKeys[i])
if err != nil {
return nil, fmt.Errorf("failed to parse public key", err)
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
buf := new(bytes.Buffer)
// encrypt each with an operator recipient
Expand All @@ -201,7 +201,7 @@ func encryptShares(shares [][]byte, operatorKeys []string) ([]paymentLib.Operato
}

if _, err = io.WriteString(w, base64.StdEncoding.EncodeToString(share)); err != nil {
return nil, fmt.Errorf("failed to write encoded ciphertext to encrypted buffer", err)
return nil, fmt.Errorf("failed to write encoded ciphertext to encrypted buffer: %w", err)
}

// Cannot defer this close because we are writing and using this writer in a loop. If this
Expand Down
22 changes: 14 additions & 8 deletions services/payments/statemachine.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,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 All @@ -218,20 +220,24 @@ func (s *Service) DriveTransaction(
// current persisted state
history, err := s.datastore.GetPaymentStateHistory(ctx, state.DocumentID)
if err != nil {
return fmt.Errorf("failed to get history from document id", err)
return fmt.Errorf("failed to get history from document id: %w", err)
}
persistedState, err := history.GetAuthenticatedPaymentState(
s.verifierStore,
state.DocumentID,
)
if err != nil {
return fmt.Errorf("failed to validate payment state history", err)
return fmt.Errorf("failed to validate payment state history: %w", err)
}
// If there is idempotency data in qldb and it is different from the idempotency data in the
// current state it means that there was a race between two calls to Authenticate and we are
// operating on the loser. There is no risk to proceeding as long as we retain the winner
// idempotency.
if persistedState != nil && !bytes.Equal(state.ExternalIdempotency, persistedState.ExternalIdempotency) {
// If there is idempotency data in qldb and in our generated state and
// it is different from the idempotency data in the current state it
// means that there was a race between two calls to Authenticate and we
// are operating on the loser. There is no risk to proceeding as long as
// we retain the winner idempotency.
if persistedState != nil &&
persistedState.ExternalIdempotency != nil &&
state.ExternalIdempotency != nil &&
!bytes.Equal(state.ExternalIdempotency, persistedState.ExternalIdempotency) {
state.ExternalIdempotency = persistedState.ExternalIdempotency
}

Expand Down
Loading

0 comments on commit 3152fae

Please sign in to comment.