Skip to content

Commit

Permalink
feat: implement broadcasting
Browse files Browse the repository at this point in the history
  • Loading branch information
johnletey committed Oct 28, 2024
1 parent 712b900 commit 100975e
Show file tree
Hide file tree
Showing 54 changed files with 8,891 additions and 76 deletions.
Binary file added .github/assets/banner_solana.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
noble-cctp-relayer
.ignore
.env


.DS_Store
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ abigen --abi ethereum/abi/ERC20.json --pkg integration_testing --type ERC20 --ou
abigen --abi ethereum/abi/MessageTransmitter.json --pkg contracts- --type MessageTransmitter --out ethereum/contracts/MessageTransmitter.go
```

### Generating Go IDL bindings

We utilize [anchor-go](https://github.com/gagliardetto/anchor-go) to generate Anchor IDL into Golang bindings.

```shell
# CCTP Message Transmitter Program
anchor-go -src solana/generated/message_transmitter.json -dst solana/generated/message_transmitter

# CCTP Token Messenger & Minter Program
anchor-go -src solana/generated/token_messenger_minter.json -dst solana/generated/token_messenger_minter
```

### Useful links
[Relayer Flow Charts](./docs/flows.md)

Expand Down
Binary file removed abigen
Binary file not shown.
40 changes: 40 additions & 0 deletions solana/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
![](../.github/assets/banner_solana.png)

# CCTP Relayer for Solana

This directory contains the necessary logic for the broader CCTP Relayer to
support Solana. It is maintained by the Noble Core Team with the intent to
enable relaying of CCTP transfers between Solana and Noble. However, due to how
the relayer is architected, can also be used to relay between Solana and any
other CCTP enabled domain.

## Configuration

Below you can find a well documented example configuration that you can use when
setting up the Solana domain in your relayer. Note that some values are left
blank due to them being secrets that shouldn't be revealed.

```yaml
chains:
solana:
# Solana specific RPC and WebSocket endpoints to interact with.
# We recommend helius.dev as a good provider of this infrastructure.
rpc: ""
ws: ""

# IDs for both the Message Transmitter and Token Messenger & Minter CCTP
# programs deployed on Solana. The below values are for Mainnet BETA.
#
# https://developers.circle.com/stablecoins/solana-programs
message-transmitter: "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd"
token-messenger-minter: "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3"

# ID for the FiatToken USDC program deployed on Solana.
#
# https://developers.circle.com/stablecoins/usdc-on-main-networks
fiat-token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"

# TODO: Explain and additional domains!
remote-tokens:
4: "0x487039debedbf32d260137b0a6f66b90962bec777250910d253781de326a716d"
```
140 changes: 116 additions & 24 deletions solana/chain.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package solana

import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/gagliardetto/solana-go/rpc/ws"

"cosmossdk.io/log"

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

Expand All @@ -23,24 +28,54 @@ type Solana struct {
RPC string
WS string
}
messageTransmitter solana.PublicKey
rpcClient *rpc.Client
wallet solana.Wallet

messageTransmitter solana.PublicKey
tokenMessengerMinter solana.PublicKey
fiatToken solana.PublicKey
remoteTokens map[types.Domain]solana.PublicKey

mu sync.Mutex
latestBlock uint64
lastFlushedBlock uint64
}

func NewSolana(
rpcEndpoint string,
wsEndpoint string,
messageTransmitter string,
) *Solana {
func NewSolana(cfg Config) *Solana {
wallet, err := solana.WalletFromPrivateKeyBase58(cfg.PrivateKey)
if err != nil {
panic(err)
}

messageTransmitter := solana.MustPublicKeyFromBase58(cfg.MessageTransmitter)
message_transmitter.SetProgramID(messageTransmitter)

tokenMessengerMinter := solana.MustPublicKeyFromBase58(cfg.TokenMessengerMinter)
token_messenger_minter.SetProgramID(tokenMessengerMinter)

remoteTokens := make(map[types.Domain]solana.PublicKey)
for domain, rawRemoteToken := range cfg.RemoteTokens {
if strings.HasPrefix(rawRemoteToken, "0x") {
remoteToken := make([]byte, 32)
tmpRemoteToken := common.FromHex(rawRemoteToken)
copy(remoteToken[32-len(tmpRemoteToken):], tmpRemoteToken)

remoteTokens[domain] = solana.PublicKeyFromBytes(remoteToken)
} else {
panic("unsupported remote token: " + rawRemoteToken)
}
}

return &Solana{
endpoints: struct {
RPC string
WS string
}{RPC: rpcEndpoint, WS: wsEndpoint},
messageTransmitter: solana.MustPublicKeyFromBase58(messageTransmitter),
}{RPC: cfg.RPC, WS: cfg.WS},
wallet: *wallet,
messageTransmitter: messageTransmitter,
tokenMessengerMinter: tokenMessengerMinter,
fiatToken: solana.MustPublicKeyFromBase58(cfg.FiatToken),
remoteTokens: remoteTokens,
}
}

Expand Down Expand Up @@ -69,15 +104,14 @@ func (s *Solana) SetLatestBlock(block uint64) {
// LastFlushedBlock implements the types.Chain interface.
func (s *Solana) LastFlushedBlock() uint64 { return s.lastFlushedBlock }

// IsDestinationCaller implements the types.Chain interface.
// TODO: Implement!!!
func (s *Solana) IsDestinationCaller(_ []byte) (isCaller bool, readableAddress string) {
return false, ""
// IsDestinationCaller checks if the relayer wallet is the specified destination caller.
func (s *Solana) IsDestinationCaller(destinationCaller []byte) (isCaller bool, readableAddress string) {
return bytes.Equal(destinationCaller, s.wallet.PublicKey().Bytes()), s.wallet.PublicKey().String()
}

// InitializeClients implements the types.Chain interface.
// NOTE: This is left empty intentionally, as there are no Solana clients to initialize.
// InitializeClients creates a new client for a Solana RPC endpoint.
func (s *Solana) InitializeClients(_ context.Context, _ log.Logger) error {
s.rpcClient = rpc.New(s.endpoints.RPC)
return nil
}

Expand All @@ -86,7 +120,7 @@ func (s *Solana) InitializeClients(_ context.Context, _ log.Logger) error {
func (s *Solana) CloseClients() error { return nil }

// InitializeBroadcaster implements the types.Chain interface.
// TODO: Implement!!!
// NOTE: This is left empty intentionally, as there is no Solana broadcaster to initialize.
func (s *Solana) InitializeBroadcaster(_ context.Context, _ log.Logger, _ *types.SequenceMap) error {
return nil
}
Expand Down Expand Up @@ -129,19 +163,55 @@ func (s *Solana) StartListener(ctx context.Context, logger log.Logger, processin
}
}

// Broadcast implements the types.Chain interfaces.
// TODO: Implement!!!
func (s *Solana) Broadcast(_ context.Context, _ log.Logger, _ []*types.MessageState, _ *types.SequenceMap, _ *relayer.PromMetrics) error {
return nil
// Broadcast receives and executes a list of messages from other domains to Solana.
func (s *Solana) Broadcast(ctx context.Context, _ log.Logger, inputs []*types.MessageState, _ *types.SequenceMap, _ *relayer.PromMetrics) error {
var instructions []solana.Instruction

for _, input := range inputs {
instruction := message_transmitter.NewReceiveMessageInstructionBuilder()

instruction.SetParams(message_transmitter.ReceiveMessageParams{
Message: input.MsgSentBytes,
Attestation: common.FromHex(input.Attestation),
})
err := instruction.SetAccounts(s.GetReceiveMessageAccounts(input))
if err != nil {
return err
}

instructions = append(instructions, instruction.Build())
}

recent, err := s.rpcClient.GetRecentBlockhash(ctx, rpc.CommitmentFinalized)
if err != nil {
return err
}
tx, err := solana.NewTransaction(instructions, recent.Value.Blockhash)
if err != nil {
return err
}

_, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
if key == s.wallet.PublicKey() {
return &s.wallet.PrivateKey
}

return nil
})
if err != nil {
return err
}

_, err = s.rpcClient.SendTransaction(ctx, tx)
return err
}

// TrackLatestBlockHeight continuously queries Solana for the latest block height.
// TODO: Ensure we are querying finalized blocks only!
func (s *Solana) TrackLatestBlockHeight(ctx context.Context, logger log.Logger, metrics *relayer.PromMetrics) {
domain := fmt.Sprint(s.Domain())

updateBlockHeight := func() {
blockHeight, err := s.GetBlockHeight(ctx)
blockHeight, err := s.rpcClient.GetBlockHeight(ctx, rpc.CommitmentFinalized)
if err != nil {
logger.Error("Unable to query Solana's block height", "err", err)
} else {
Expand All @@ -167,6 +237,28 @@ func (s *Solana) TrackLatestBlockHeight(ctx context.Context, logger log.Logger,
}
}

// WalletBalanceMetric implements the types.Chain interface.
// TODO: Implement!!!
func (s *Solana) WalletBalanceMetric(_ context.Context, _ log.Logger, _ *relayer.PromMetrics) {}
// WalletBalanceMetric continuously queries Solana for the SOL balance of the relayer wallet.
func (s *Solana) WalletBalanceMetric(ctx context.Context, logger log.Logger, metrics *relayer.PromMetrics) {
updateBalance := func() {
res, err := s.rpcClient.GetBalance(ctx, s.wallet.PublicKey(), rpc.CommitmentFinalized)
if err != nil {
logger.Error("Unable to query relayer wallet balance", "err", err)
} else if metrics != nil {
balance := float64(res.Value) / 1e9
metrics.SetWalletBalance(s.Name(), s.wallet.PublicKey().String(), "SOL", balance)
}
}

updateBalance()

for {
timer := time.NewTimer(5 * time.Minute)
select {
case <-timer.C:
updateBalance()
case <-ctx.Done():
timer.Stop()
return
}
}
}
9 changes: 7 additions & 2 deletions solana/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ type Config struct {
RPC string `yaml:"rpc"`
WS string `yaml:"ws"`

MessageTransmitter string `yaml:"message-transmitter"`
MessageTransmitter string `yaml:"message-transmitter"`
TokenMessengerMinter string `yaml:"token-messenger-minter"`
FiatToken string `yaml:"fiat-token"`
RemoteTokens map[types.Domain]string `json:"remote-tokens"`

PrivateKey string `yaml:"private-key"`
}

func (cfg Config) Chain(_ string) (types.Chain, error) {
return NewSolana(cfg.RPC, cfg.WS, cfg.MessageTransmitter), nil
return NewSolana(cfg), nil
}
3 changes: 0 additions & 3 deletions solana/generated/README.md

This file was deleted.

Loading

0 comments on commit 100975e

Please sign in to comment.