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

feat: Monitor the transaction status with CheckSignature #2

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -80,30 +83,43 @@ 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

This library is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## 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`
32 changes: 32 additions & 0 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
88 changes: 83 additions & 5 deletions engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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"

Expand Down Expand Up @@ -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)
})

Expand All @@ -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)
})

Expand Down Expand Up @@ -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)
})
}
7 changes: 5 additions & 2 deletions jup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package jupitergo_test
import (
"context"
"errors"
"net/http"
"testing"

"github.com/test-go/testify/require"
Expand Down Expand Up @@ -35,7 +36,8 @@ func (j jupApiMock) GetQuoteWithResponse(
}

return &openapi.GetQuoteResponse{
JSON200: testQuoteResponse,
JSON200: testQuoteResponse,
HTTPResponse: &http.Response{StatusCode: http.StatusOK},
}, nil
}

Expand All @@ -49,7 +51,8 @@ func (j jupApiMock) PostSwapWithResponse(
}

return &openapi.PostSwapResponse{
JSON200: testSwapResponse,
JSON200: testSwapResponse,
HTTPResponse: &http.Response{StatusCode: http.StatusOK},
}, nil
}

Expand Down
Loading