Skip to content

Commit

Permalink
refactor: retreive sent messages directly from event accounts
Browse files Browse the repository at this point in the history
johnletey committed Oct 24, 2024
1 parent 0daa546 commit 933ac9e
Showing 3 changed files with 197 additions and 79 deletions.
145 changes: 66 additions & 79 deletions solana/listener.go
Original file line number Diff line number Diff line change
@@ -2,115 +2,102 @@ package solana

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"encoding/base64"
"encoding/hex"
"time"

cctptypes "github.com/circlefin/noble-cctp/x/cctp/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/google/uuid"

"github.com/cosmos/btcutil/base58"

"github.com/strangelove-ventures/noble-cctp-relayer/solana/generated/message_transmitter"
"github.com/strangelove-ventures/noble-cctp-relayer/types"
)

// Transaction defines the expected response structure when using Solana's
// getTransaction RPC method with the "jsonParsed" encoding type.
type Transaction struct {
Result struct {
Meta struct {
InnerInstructions []struct {
Index int `json:"index"`
Instructions []struct {
Data string `json:"data"`
} `json:"instructions"`
} `json:"innerInstructions"`
} `json:"meta"`
Transaction struct {
Message struct {
AccountKeys []struct {
PubKey string `json:"pubKey"`
Signer bool `json:"signer"`
Writable bool `json:"writable"`
} `json:"accountKeys"`
Instructions []struct {
Data string `json:"data"`
} `json:"instructions"`
} `json:"message"`
} `json:"transaction"`
} `json:"result"`
}

// NewTransactionRequest is a utility that forms a request for using Solana's
// getTransaction RPC method with the "jsonParsed" encoding type.
func (s *Solana) NewTransactionRequest(endpoint string, hash string) (*http.Request, error) {
data := fmt.Sprintf(`{"method":"getTransaction","jsonrpc":"2.0","params":["%s",{"encoding":"jsonParsed","commitment":"confirmed","maxSupportedTransactionVersion":0}],"id":"%s"}`, hash, uuid.New().String())

// TODO: Receive the context from the main listener!
return http.NewRequestWithContext(context.Background(), http.MethodPost, endpoint, strings.NewReader(data))
// Instruction is a utility type to store accounts and data for a specific instruction.
type Instruction struct {
Accounts []*solana.AccountMeta
Data []byte
}

// ParseTransaction is a utility that fetches a transaction from the Solana RPC
// and returns all messages sent via CCTP. It does not apply any filtering.
func (s *Solana) ParseTransaction(endpoint string, hash string) (events []message_transmitter.SendMessageWithCallerParams, err error) {
req, err := s.NewTransactionRequest(endpoint, hash)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
func (s *Solana) ParseTransaction(ctx context.Context, endpoint string, hash string) (events []types.MessageState, err error) {
tx, err := s.GetTransaction(ctx, endpoint, hash)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var tx Transaction
if err = json.Unmarshal(body, &tx); err != nil {
return nil, err
}

var accounts []*solana.AccountMeta
allAccounts := make(map[string]*solana.AccountMeta)
for _, account := range tx.Result.Transaction.Message.AccountKeys {
accounts = append(accounts, &solana.AccountMeta{
allAccounts[account.PubKey] = &solana.AccountMeta{
PublicKey: solana.MustPublicKeyFromBase58(account.PubKey),
IsWritable: account.Writable,
IsSigner: account.Signer,
})
}
}

var instructions [][]byte
var instructions []Instruction
for _, tmp := range tx.Result.Meta.InnerInstructions {
for _, instruction := range tmp.Instructions {
if instruction.Data != "" {
instructions = append(instructions, base58.Decode(instruction.Data))
if instruction.Accounts != nil && instruction.Data != "" {
var accounts []*solana.AccountMeta
for _, account := range instruction.Accounts {
accounts = append(accounts, allAccounts[account])
}

instructions = append(instructions, Instruction{
Accounts: accounts,
Data: base58.Decode(instruction.Data),
})
}
}
}
for _, instruction := range tx.Result.Transaction.Message.Instructions {
if instruction.Data != "" {
instructions = append(instructions, base58.Decode(instruction.Data))
}
}

for _, bz := range instructions {
rawEvent, err := message_transmitter.DecodeInstruction(accounts, bz)
for _, rawInstruction := range instructions {
decodedInstruction, err := message_transmitter.DecodeInstruction(rawInstruction.Accounts, rawInstruction.Data)
if err == nil {
switch message_transmitter.InstructionIDToName(rawEvent.TypeID) {
case "SendMessage":
event := rawEvent.Impl.(*message_transmitter.SendMessage)
events = append(events, message_transmitter.SendMessageWithCallerParams{
DestinationDomain: event.Params.DestinationDomain,
Recipient: event.Params.Recipient,
MessageBody: event.Params.MessageBody,
DestinationCaller: solana.PublicKey{},
switch instruction := decodedInstruction.Impl.(type) {
case *message_transmitter.SendMessage:
case *message_transmitter.SendMessageWithCaller:
account := instruction.GetMessageSentEventDataAccount().PublicKey.String()
info, err := s.GetAccountInfo(ctx, endpoint, account)
if err != nil {
return nil, err
}

bz, err := base64.StdEncoding.DecodeString(info.Result.Value.Data[0])
if err != nil {
return nil, err
}

var event message_transmitter.MessageSent
err = event.UnmarshalWithDecoder(bin.NewBorshDecoder(bz))
if err != nil {
return nil, err
}

msg, err := new(cctptypes.Message).Parse(event.Message)
if err != nil {
return nil, err
}

events = append(events, types.MessageState{
IrisLookupID: hex.EncodeToString(crypto.Keccak256(event.Message)),
Status: types.Created,
SourceDomain: types.Domain(msg.SourceDomain),
DestDomain: types.Domain(msg.DestinationDomain),
SourceTxHash: hash,
MsgSentBytes: event.Message,
MsgBody: msg.MessageBody,
DestinationCaller: msg.DestinationCaller,
Created: time.Now(),
Updated: time.Now(),
Nonce: msg.Nonce,
})
case "SendMessageWithCaller":
event := rawEvent.Impl.(*message_transmitter.SendMessageWithCaller)
events = append(events, *event.Params)
}
}
}
2 changes: 2 additions & 0 deletions solana/listener_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package solana_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"
@@ -16,6 +17,7 @@ import (
func TestParseTransaction(t *testing.T) {
// ACT: Attempt to fetch and parse a transaction.
events, err := new(solana.Solana).ParseTransaction(
context.Background(),
"https://corie-nhz8jx-fast-mainnet.helius-rpc.com",
"4XhuTtTHxNFDfGn6A7ngvqT2dNoxRQFK7kpZwNY25gxXU5SPFCAk6ihj9JJdq5g7UMb7MSwyTt5r3TJbM4RgMVyJ",
)
129 changes: 129 additions & 0 deletions solana/rpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package solana

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"

"github.com/google/uuid"
)

// AccountInfo defines the expected response structure when using Solana's
// getAccountInfo RPC method with the "base64" encoding type.
type AccountInfo struct {
Result struct {
Value struct {
Data []string `json:"data"`
} `json:"value"`
} `json:"result"`
}

// Transaction defines the expected response structure when using Solana's
// getTransaction RPC method with the "jsonParsed" encoding type.
type Transaction struct {
Result struct {
Meta struct {
InnerInstructions []struct {
Index int `json:"index"`
Instructions []struct {
Accounts []string `json:"accounts"`
Data string `json:"data"`
} `json:"instructions"`
} `json:"innerInstructions"`
} `json:"meta"`
Transaction struct {
Message struct {
AccountKeys []struct {
PubKey string `json:"pubKey"`
Signer bool `json:"signer"`
Writable bool `json:"writable"`
} `json:"accountKeys"`
Instructions []struct {
Accounts []string `json:"accounts"`
Data string `json:"data"`
} `json:"instructions"`
} `json:"message"`
} `json:"transaction"`
} `json:"result"`
}

// GetAccountInfo is a utility that returns the response of a request using
// Solana's getAccountInfo RPC method with the "base64" encoding type.
func (s *Solana) GetAccountInfo(ctx context.Context, endpoint string, account string) (*AccountInfo, error) {
data := fmt.Sprintf(`{
"jsonrpc": "2.0",
"method": "getAccountInfo",
"id": "%s",
"params": [
"%s",
{ "encoding": "base64" }
]
}`, uuid.New().String(), account)

req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(data))
if err != nil {
return nil, err
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

var info AccountInfo
if err = json.Unmarshal(body, &info); err != nil {
return nil, err
}

return &info, nil
}

// GetTransaction is a utility that returns the response of a request using
// Solana's getTransaction RPC method with the "jsonParsed" encoding type.
func (s *Solana) GetTransaction(ctx context.Context, endpoint string, hash string) (*Transaction, error) {
data := fmt.Sprintf(`{
"jsonrpc": "2.0",
"method": "getTransaction",
"id": "%s",
"params": [
"%s",
{
"encoding": "jsonParsed",
"commitment": "confirmed",
"maxSupportedTransactionVersion": 0
}
]
}`, uuid.New().String(), hash)

req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(data))
if err != nil {
return nil, err
}

res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

var tx Transaction
if err = json.Unmarshal(body, &tx); err != nil {
return nil, err
}

return &tx, nil
}

0 comments on commit 933ac9e

Please sign in to comment.