2023-07-04 11:25:08 -07:00
|
|
|
package chains
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/mr-tron/base58"
|
2024-03-19 11:47:43 -07:00
|
|
|
"github.com/wormhole-foundation/wormhole-explorer/common/pool"
|
2023-12-04 11:29:38 -08:00
|
|
|
"github.com/wormhole-foundation/wormhole-explorer/common/types"
|
2024-03-19 11:47:43 -07:00
|
|
|
"github.com/wormhole-foundation/wormhole-explorer/txtracker/internal/metrics"
|
|
|
|
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
|
|
|
"go.uber.org/zap"
|
2023-07-04 11:25:08 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
type solanaTransactionSignature struct {
|
2024-01-31 14:41:41 -08:00
|
|
|
BlockTime int64 `json:"blockTime"`
|
|
|
|
Signature string `json:"signature"`
|
|
|
|
Err interface{} `json:"err"`
|
2023-07-04 11:25:08 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
type solanaGetTransactionResponse struct {
|
|
|
|
BlockTime int64 `json:"blockTime"`
|
|
|
|
Meta struct {
|
|
|
|
InnerInstructions []struct {
|
|
|
|
Instructions []struct {
|
|
|
|
ParsedInstruction struct {
|
|
|
|
Type_ string `json:"type"`
|
|
|
|
Info struct {
|
|
|
|
Account string `json:"account"`
|
|
|
|
Amount string `json:"amount"`
|
|
|
|
Authority string `json:"authority"`
|
|
|
|
Destination string `json:"destination"`
|
|
|
|
Source string `json:"source"`
|
|
|
|
} `json:"info"`
|
|
|
|
} `json:"parsed"`
|
|
|
|
} `json:"instructions"`
|
|
|
|
} `json:"innerInstructions"`
|
2024-01-31 14:41:41 -08:00
|
|
|
Err interface{} `json:"err"`
|
2023-07-04 11:25:08 -07:00
|
|
|
} `json:"meta"`
|
|
|
|
Transaction struct {
|
|
|
|
Message struct {
|
|
|
|
AccountKeys []struct {
|
|
|
|
Pubkey string `json:"pubkey"`
|
|
|
|
Signer bool `json:"signer"`
|
|
|
|
} `json:"accountKeys"`
|
|
|
|
} `json:"message"`
|
|
|
|
Signatures []string `json:"signatures"`
|
|
|
|
} `json:"transaction"`
|
|
|
|
}
|
|
|
|
|
2023-12-01 09:15:19 -08:00
|
|
|
type getTransactionConfig struct {
|
|
|
|
Encoding string `json:"encoding"`
|
|
|
|
MaxSupportedTransactionVersion int `json:"maxSupportedTransactionVersion"`
|
|
|
|
}
|
|
|
|
|
2023-12-04 10:30:16 -08:00
|
|
|
type apiSolana struct {
|
|
|
|
timestamp *time.Time
|
|
|
|
}
|
|
|
|
|
2024-03-19 11:47:43 -07:00
|
|
|
func (a *apiSolana) FetchSolanaTx(
|
|
|
|
ctx context.Context,
|
|
|
|
pool *pool.Pool,
|
|
|
|
txHash string,
|
|
|
|
metrics metrics.Metrics,
|
|
|
|
logger *zap.Logger,
|
|
|
|
) (*TxDetail, error) {
|
|
|
|
|
|
|
|
// get rpc sorted by score and priority.
|
|
|
|
rpcs := pool.GetItems()
|
|
|
|
if len(rpcs) == 0 {
|
|
|
|
return nil, ErrChainNotSupported
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the transaction from the Solana node API.
|
|
|
|
var txDetail *TxDetail
|
|
|
|
var err error
|
|
|
|
for _, rpc := range rpcs {
|
|
|
|
// Wait for the RPC rate limiter
|
|
|
|
rpc.Wait(ctx)
|
|
|
|
txDetail, err = a.fetchSolanaTx(ctx, rpc.Id, txHash)
|
|
|
|
if txDetail != nil {
|
|
|
|
metrics.IncCallRpcSuccess(uint16(sdk.ChainIDSolana), rpc.Description)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
metrics.IncCallRpcError(uint16(sdk.ChainIDSolana), rpc.Description)
|
|
|
|
logger.Debug("Failed to fetch transaction from Solana node", zap.String("url", rpc.Id), zap.Error(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return txDetail, err
|
|
|
|
}
|
|
|
|
|
2023-12-04 10:30:16 -08:00
|
|
|
func (a *apiSolana) fetchSolanaTx(
|
2023-07-04 11:25:08 -07:00
|
|
|
ctx context.Context,
|
|
|
|
baseUrl string,
|
|
|
|
txHash string,
|
|
|
|
) (*TxDetail, error) {
|
|
|
|
|
|
|
|
// Initialize RPC client
|
|
|
|
client, err := rpcDialContext(ctx, baseUrl)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to initialize RPC client: %w", err)
|
|
|
|
}
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
// Decode txHash bytes
|
|
|
|
// TODO: remove this when the fly fixes all txHash for Solana
|
|
|
|
h, err := hex.DecodeString(txHash)
|
|
|
|
if err != nil {
|
|
|
|
h, err = base58.Decode(txHash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to decode from hex txHash=%s: %w", txHash, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var sigs []solanaTransactionSignature
|
2023-12-04 11:29:38 -08:00
|
|
|
nativeTxHash := txHash
|
|
|
|
txHashType, err := types.ParseTxHash(txHash)
|
|
|
|
isNotNativeTxHash := err != nil || !txHashType.IsSolanaTxHash()
|
|
|
|
if isNotNativeTxHash {
|
|
|
|
// Get transaction signatures for the given account
|
|
|
|
{
|
2024-03-19 11:47:43 -07:00
|
|
|
err = client.CallContext(ctx, &sigs, "getSignaturesForAddress", base58.Encode(h))
|
2023-12-04 11:29:38 -08:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to get signatures for account: %w (%+v)", err, err)
|
|
|
|
}
|
|
|
|
if len(sigs) == 0 {
|
|
|
|
return nil, ErrTransactionNotFound
|
|
|
|
}
|
2023-12-04 10:30:16 -08:00
|
|
|
|
2023-12-04 11:29:38 -08:00
|
|
|
if len(sigs) == 1 {
|
|
|
|
nativeTxHash = sigs[0].Signature
|
|
|
|
} else {
|
|
|
|
for _, sig := range sigs {
|
2024-01-31 14:41:41 -08:00
|
|
|
|
|
|
|
if a.timestamp != nil && sig.BlockTime == a.timestamp.Unix() && sig.Err == nil {
|
2023-12-04 11:29:38 -08:00
|
|
|
nativeTxHash = sig.Signature
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if nativeTxHash == "" {
|
|
|
|
return nil, fmt.Errorf("can't get signature, but found %d", len(sigs))
|
2023-12-04 10:30:16 -08:00
|
|
|
}
|
|
|
|
}
|
2023-07-04 11:25:08 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch the portal token bridge transaction
|
|
|
|
var response solanaGetTransactionResponse
|
|
|
|
{
|
2024-03-19 11:47:43 -07:00
|
|
|
err = client.CallContext(ctx, &response, "getTransaction", nativeTxHash,
|
2023-12-01 09:15:19 -08:00
|
|
|
getTransactionConfig{
|
|
|
|
Encoding: "jsonParsed",
|
|
|
|
MaxSupportedTransactionVersion: 0,
|
|
|
|
})
|
2023-07-04 11:25:08 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to get tx by signature: %w", err)
|
|
|
|
}
|
2023-12-04 10:30:16 -08:00
|
|
|
if len(sigs) == 1 {
|
|
|
|
if len(response.Meta.InnerInstructions) == 0 {
|
|
|
|
return nil, fmt.Errorf("response.Meta.InnerInstructions is empty")
|
|
|
|
}
|
|
|
|
if len(response.Meta.InnerInstructions[0].Instructions) == 0 {
|
|
|
|
return nil, fmt.Errorf("response.Meta.InnerInstructions[0].Instructions is empty")
|
|
|
|
}
|
2023-07-04 11:25:08 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// populate the response object
|
|
|
|
txDetail := TxDetail{
|
2023-12-04 10:30:16 -08:00
|
|
|
NativeTxHash: nativeTxHash,
|
2023-07-04 11:25:08 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// set sender/receiver
|
|
|
|
for i := range response.Transaction.Message.AccountKeys {
|
|
|
|
if response.Transaction.Message.AccountKeys[i].Signer {
|
|
|
|
txDetail.From = response.Transaction.Message.AccountKeys[i].Pubkey
|
2024-03-07 06:21:26 -08:00
|
|
|
// https://github.com/wormhole-foundation/wormhole-explorer/issues/1142
|
|
|
|
// we get the first signer as the origintx from.
|
|
|
|
break
|
2023-07-04 11:25:08 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if txDetail.From == "" {
|
|
|
|
return nil, fmt.Errorf("failed to find source account")
|
|
|
|
}
|
|
|
|
|
|
|
|
return &txDetail, nil
|
|
|
|
}
|