diff --git a/README.md b/README.md index 30449b9..de01c91 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,14 @@ func main() { ``` Once you have the swap instructions, you can use the [Solana engine](engine.go) to sign and send the transaction. +Once a transaction is sent on-chain it doesn't mean that the swap is completed. You should monitor the transaction status and confirm the swap is completed. ```go package main import ( + "time" + "github.com/ilkamo/jupiter-go" ) @@ -80,23 +83,36 @@ func main() { // handle me } - // Sign and send the transaction + // Create a Solana engine eng, err := jupitergo.NewSolanaEngine(wallet, "https://api.mainnet-beta.solana.com") if err != nil { // handle me } - // Sign the transaction + // Sign and send the transaction signedTx, err := eng.SendSwapOnChain(ctx, swap) if err != nil { // handle me } + + // wait a bit to let the transaction propagate to the network - this is just an example and not a best practice + // you could use a ticker or wait until we implement the WebSocket monitoring ;) + time.Sleep(20 * time.Second) + + // Get the status of the transaction (pull the status from the blockchain at intervals until the transaction is confirmed) + confirmed, err := eng.CheckSignature(ctx, signedTx) + if err != nil { + panic(err) + } } + ``` -## TODO +## TODOs -Once a transaction is sent on-chain it doesn't mean that the swap is completed. You should monitor the transaction status and confirm the swap is completed. This library doesn't provide a way to monitor the transaction status yet but it's on the roadmap. +- Add more examples +- Add more tests +- Use WebSockets to monitor the transaction status ## License @@ -104,6 +120,6 @@ This library is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Donate -If you find this library useful, consider donating some JUP or Solana to the following addresses: +If you find this library useful and want to support its development, consider donating some JUP/Solana to the following address: `BXzmfHxfEMcMj8hDccUNdrwXVNeybyfb2iV2nktE1VnJ` diff --git a/engine.go b/engine.go index 8eac7c7..dd3015b 100644 --- a/engine.go +++ b/engine.go @@ -24,6 +24,11 @@ type SolanaClientRPC interface { ctx context.Context, commitment rpc.CommitmentType, ) (out *rpc.GetLatestBlockhashResult, err error) + GetSignatureStatuses( + ctx context.Context, + searchTransactionHistory bool, + transactionSignatures ...solana.Signature, + ) (out *rpc.GetSignatureStatusesResult, err error) } type SolanaEngine struct { @@ -109,3 +114,30 @@ func (e SolanaEngine) SendSwapOnChain(ctx context.Context, swap openapi.SwapResp return TxID(sig.String()), nil } + +// CheckSignature checks if a transaction with the given signature has been confirmed on-chain +func (e SolanaEngine) CheckSignature(ctx context.Context, tx TxID) (bool, error) { + sig, err := solana.SignatureFromBase58(string(tx)) + if err != nil { + return false, fmt.Errorf("could not convert signature from base58: %w", err) + } + + status, err := e.solanaClientRPC.GetSignatureStatuses(ctx, false, sig) + if err != nil { + return false, fmt.Errorf("could not get signature status: %w", err) + } + + if len(status.Value) == 0 { + return false, fmt.Errorf("could not confirm transaction: no valid status") + } + + if status.Value[0].ConfirmationStatus != rpc.ConfirmationStatusFinalized { + return false, fmt.Errorf("transaction not finalized yet") + } + + if status.Value[0].Err != nil { + return true, fmt.Errorf("transaction confirmed with error: %s", status.Value[0].Err) + } + + return true, nil +} diff --git a/engine_test.go b/engine_test.go index 4e3be52..fb2e739 100644 --- a/engine_test.go +++ b/engine_test.go @@ -17,8 +17,14 @@ import ( type rpcMock struct { shouldFailGetLatestBlockhash bool shouldFailSendTransaction bool + shouldFailGetSignatureStatus bool } +var ( + testSignature = "24jRjMP3medE9iMqVSPRbkwfe9GdPmLfeftKPuwRHZdYTZJ6UyzNMGGKo4BHrTu2zVj4CgFF3CEuzS79QXUo2CMC" + processingSignature = "24jRjMP3medE9iMqVSPRbkwfe9GdPmLfeftKPuwRHZdYTZJ6UyzNMGGKo4BHrTu2zVj4CgFF3CEuzS79QXUo2CPC" +) + func (r rpcMock) SendTransactionWithOpts( _ context.Context, _ *solana.Transaction, @@ -28,9 +34,7 @@ func (r rpcMock) SendTransactionWithOpts( return solana.Signature{}, errors.New("mocked error") } - return solana.MustSignatureFromBase58( - "24jRjMP3medE9iMqVSPRbkwfe9GdPmLfeftKPuwRHZdYTZJ6UyzNMGGKo4BHrTu2zVj4CgFF3CEuzS79QXUo2CMC", - ), nil + return solana.MustSignatureFromBase58(testSignature), nil } func (r rpcMock) GetLatestBlockhash( @@ -49,6 +53,34 @@ func (r rpcMock) GetLatestBlockhash( }, nil } +func (r rpcMock) GetSignatureStatuses( + _ context.Context, + _ bool, + sign ...solana.Signature, +) (out *rpc.GetSignatureStatusesResult, err error) { + if r.shouldFailGetSignatureStatus { + return nil, errors.New("mocked error") + } + + if sign[0].Equals(solana.MustSignatureFromBase58(processingSignature)) { + return &rpc.GetSignatureStatusesResult{ + Value: []*rpc.SignatureStatusesResult{ + { + ConfirmationStatus: rpc.ConfirmationStatusProcessed, + }, + }, + }, nil + } + + return &rpc.GetSignatureStatusesResult{ + Value: []*rpc.SignatureStatusesResult{ + { + ConfirmationStatus: rpc.ConfirmationStatusFinalized, + }, + }, + }, nil +} + func TestNewSolanaEngine(t *testing.T) { testPk := "5473ZnvEhn35BdcCcPLKnzsyP6TsgqQrNFpn4i2gFegFiiJLyWginpa9GoFn2cy6Aq2EAuxLt2u2bjFDBPvNY6nw" @@ -96,7 +128,7 @@ func TestNewSolanaEngine(t *testing.T) { }) require.NoError(t, err) - expectedTxID := jupitergo.TxID("24jRjMP3medE9iMqVSPRbkwfe9GdPmLfeftKPuwRHZdYTZJ6UyzNMGGKo4BHrTu2zVj4CgFF3CEuzS79QXUo2CMC") + expectedTxID := jupitergo.TxID(testSignature) require.Equal(t, expectedTxID, txID) }) @@ -114,7 +146,7 @@ func TestNewSolanaEngine(t *testing.T) { }) require.NoError(t, err) - expectedTxID := jupitergo.TxID("24jRjMP3medE9iMqVSPRbkwfe9GdPmLfeftKPuwRHZdYTZJ6UyzNMGGKo4BHrTu2zVj4CgFF3CEuzS79QXUo2CMC") + expectedTxID := jupitergo.TxID(testSignature) require.Equal(t, expectedTxID, txID) }) @@ -147,4 +179,50 @@ func TestNewSolanaEngine(t *testing.T) { }) require.EqualError(t, err, "could not send transaction: mocked error") }) + + t.Run("error when getting the signature status", func(t *testing.T) { + eng, err := jupitergo.NewSolanaEngine( + wallet, + "", + jupitergo.WithSolanaClientRPC(rpcMock{shouldFailGetSignatureStatus: true}), + ) + require.NoError(t, err) + + _, err = eng.CheckSignature( + context.TODO(), + jupitergo.TxID(testSignature), + ) + require.EqualError(t, err, "could not get signature status: mocked error") + }) + + t.Run("transaction still in process when getting signature status", func(t *testing.T) { + eng, err := jupitergo.NewSolanaEngine( + wallet, + "", + jupitergo.WithSolanaClientRPC(rpcMock{}), + ) + require.NoError(t, err) + + _, err = eng.CheckSignature( + context.TODO(), + jupitergo.TxID(processingSignature), + ) + require.EqualError(t, err, "transaction not finalized yet") + }) + + t.Run("transaction confirmed when getting signature status", func(t *testing.T) { + eng, err := jupitergo.NewSolanaEngine( + wallet, + "", + jupitergo.WithSolanaClientRPC(rpcMock{}), + ) + require.NoError(t, err) + + confirmed, err := eng.CheckSignature( + context.TODO(), + jupitergo.TxID(testSignature), + ) + require.NoError(t, err) + require.True(t, confirmed) + }) } diff --git a/jup_test.go b/jup_test.go index 013eac9..14ca2c2 100644 --- a/jup_test.go +++ b/jup_test.go @@ -3,6 +3,7 @@ package jupitergo_test import ( "context" "errors" + "net/http" "testing" "github.com/test-go/testify/require" @@ -35,7 +36,8 @@ func (j jupApiMock) GetQuoteWithResponse( } return &openapi.GetQuoteResponse{ - JSON200: testQuoteResponse, + JSON200: testQuoteResponse, + HTTPResponse: &http.Response{StatusCode: http.StatusOK}, }, nil } @@ -49,7 +51,8 @@ func (j jupApiMock) PostSwapWithResponse( } return &openapi.PostSwapResponse{ - JSON200: testSwapResponse, + JSON200: testSwapResponse, + HTTPResponse: &http.Response{StatusCode: http.StatusOK}, }, nil }