Skip to content

Commit

Permalink
feat: solana blockchain support (#1509)
Browse files Browse the repository at this point in the history
Solana support
  • Loading branch information
graham-chainlink authored Dec 23, 2024
1 parent c0699a9 commit 67350e9
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 180 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/framework-golden-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ jobs:
config: smoke.toml
count: 1
timeout: 10m
- name: TestSolanaSmoke
config: smoke_solana.toml
count: 1
timeout: 10m
- name: TestUpgrade
config: upgrade.toml
count: 1
Expand Down
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
- [Components](framework/components/overview.md)
- [Blockchains](framework/components/blockchains/overview.md)
- [EVM](framework/components/blockchains/evm.md)
- [Solana](framework/components/blockchains/solana.md)
- [Optimism Stack]()
- [Arbitrum Stack]()
- [Chainlink](framework/components/chainlink.md)
Expand Down
64 changes: 64 additions & 0 deletions book/src/framework/components/blockchains/solana.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Solana Blockchain Client

Since `Solana` doesn't have official image for `arm64` we built it, images we use are:
```
amd64 solanalabs/solana:v1.18.26 - used in CI
arm64 f4hrenh9it/solana:latest - used locally
```

## Configuration
```toml
[blockchain_a]
type = "solana"
# public key for mint
public_key = "9n1pyVGGo6V4mpiSDMVay5As9NurEkY283wwRk1Kto2C"
# contracts directory, programs
contracts_dir = "."
# optional, in case you need some custom image
# image = "solanalabs/solana:v1.18.26"
```

## Usage
```golang
package examples

import (
"context"
"fmt"
"github.com/blocto/solana-go-sdk/client"
"github.com/smartcontractkit/chainlink-testing-framework/framework"
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
"github.com/stretchr/testify/require"
"testing"
)

type CfgSolana struct {
BlockchainA *blockchain.Input `toml:"blockchain_a" validate:"required"`
}

func TestSolanaSmoke(t *testing.T) {
in, err := framework.Load[CfgSolana](t)
require.NoError(t, err)

bc, err := blockchain.NewBlockchainNetwork(in.BlockchainA)
require.NoError(t, err)

t.Run("test something", func(t *testing.T) {
// use internal URL to connect chainlink nodes
_ = bc.Nodes[0].DockerInternalHTTPUrl
// use host URL to deploy contracts
c := client.NewClient(bc.Nodes[0].HostHTTPUrl)
latestSlot, err := c.GetSlotWithConfig(context.Background(), client.GetSlotConfig{Commitment: "processed"})
require.NoError(t, err)
fmt.Printf("Latest slot: %v\n", latestSlot)
})
}
```

## Test Private Keys

```
Public: 9n1pyVGGo6V4mpiSDMVay5As9NurEkY283wwRk1Kto2C
Private: [11,2,35,236,230,251,215,68,220,208,166,157,229,181,164,26,150,230,218,229,41,20,235,80,183,97,20,117,191,159,228,243,130,101,145,43,51,163,139,142,11,174,113,54,206,213,188,127,131,147,154,31,176,81,181,147,78,226,25,216,193,243,136,149]
```

1 change: 1 addition & 0 deletions framework/.changeset/v0.4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add basic Solana network support
17 changes: 13 additions & 4 deletions framework/components/blockchain/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ import (

// Input is a blockchain network configuration params
type Input struct {
Type string `toml:"type" validate:"required,oneof=anvil geth besu" envconfig:"net_type"`
Image string `toml:"image"`
PullImage bool `toml:"pull_image"`
Port string `toml:"port"`
// Common EVM fields
Type string `toml:"type" validate:"required,oneof=anvil geth besu solana" envconfig:"net_type"`
Image string `toml:"image"`
PullImage bool `toml:"pull_image"`
Port string `toml:"port"`
// Not applicable to Solana, ws port for Solana is +1 of port
WSPort string `toml:"port_ws"`
ChainID string `toml:"chain_id"`
DockerCmdParamsOverrides []string `toml:"docker_cmd_params"`
Out *Output `toml:"out"`

// Solana fields
// publickey to mint when solana-test-validator starts
PublicKey string `toml:"public_key"`
ContractsDir string `toml:"contracts_dir"`
}

// Output is a blockchain network output, ChainID and one or more nodes that forms the network
Expand Down Expand Up @@ -49,6 +56,8 @@ func NewBlockchainNetwork(in *Input) (*Output, error) {
out, err = newGeth(in)
case "besu":
out, err = newBesu(in)
case "solana":
out, err = newSolana(in)
default:
return nil, fmt.Errorf("blockchain type is not supported or empty, must be 'anvil' or 'geth'")
}
Expand Down
158 changes: 158 additions & 0 deletions framework/components/blockchain/solana.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package blockchain

import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"time"

"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/go-connections/nat"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"

"github.com/smartcontractkit/chainlink-testing-framework/framework"
)

var configYmlRaw = `
json_rpc_url: http://0.0.0.0:%s
websocket_url: ws://0.0.0.0:%s
keypair_path: /root/.config/solana/cli/id.json
address_labels:
"11111111111111111111111111111111": ""
commitment: finalized
`

var idJSONRaw = `
[11,2,35,236,230,251,215,68,220,208,166,157,229,181,164,26,150,230,218,229,41,20,235,80,183,97,20,117,191,159,228,243,130,101,145,43,51,163,139,142,11,174,113,54,206,213,188,127,131,147,154,31,176,81,181,147,78,226,25,216,193,243,136,149]
`

func defaultSolana(in *Input) {
ci := os.Getenv("CI") == "true"
if in.Image == "" && !ci {
in.Image = "f4hrenh9it/solana"
}
if in.Image == "" && ci {
in.Image = "solanalabs/solana:v1.18.26"
}
if in.Port == "" {
in.Port = "8545"
}
}

func newSolana(in *Input) (*Output, error) {
defaultSolana(in)
ctx := context.Background()
containerName := framework.DefaultTCName("blockchain-node")
// Solana do not allow to set ws port, it just uses --rpc-port=N and sets WS as N+1 automatically
bindPort := fmt.Sprintf("%s/tcp", in.Port)
pp, err := strconv.Atoi(in.Port)
if err != nil {
return nil, fmt.Errorf("in.Port is not a number")
}
in.WSPort = strconv.Itoa(pp + 1)
wsBindPort := fmt.Sprintf("%s/tcp", in.WSPort)

configYml, err := os.CreateTemp("", "config.yml")
if err != nil {
return nil, err
}
configYmlRaw = fmt.Sprintf(configYmlRaw, in.Port, in.WSPort)
_, err = configYml.WriteString(configYmlRaw)
if err != nil {
return nil, err
}

idJSON, err := os.CreateTemp("", "id.json")
if err != nil {
return nil, err
}
_, err = idJSON.WriteString(idJSONRaw)
if err != nil {
return nil, err
}

contractsDir, err := filepath.Abs(in.ContractsDir)
if err != nil {
return nil, err
}

req := testcontainers.ContainerRequest{
AlwaysPullImage: in.PullImage,
Image: in.Image,
Labels: framework.DefaultTCLabels(),
Name: containerName,
ExposedPorts: []string{bindPort, wsBindPort},
Networks: []string{framework.DefaultNetworkName},
NetworkAliases: map[string][]string{
framework.DefaultNetworkName: {containerName},
},
WaitingFor: wait.ForLog("Processed Slot: 1").
WithStartupTimeout(30 * time.Second).
WithPollInterval(100 * time.Millisecond),
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = nat.PortMap{
nat.Port(bindPort): []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: bindPort,
},
},
nat.Port(wsBindPort): []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: wsBindPort,
},
},
}
h.Mounts = append(h.Mounts, mount.Mount{
Type: mount.TypeBind,
Source: contractsDir,
Target: "/programs",
ReadOnly: false,
})
},
Files: []testcontainers.ContainerFile{
{
HostFilePath: configYml.Name(),
ContainerFilePath: "/root/.config/solana/cli/config.yml",
FileMode: 0644,
},
{
HostFilePath: idJSON.Name(),
ContainerFilePath: "/root/.config/solana/cli/id.json",
FileMode: 0644,
},
},
Entrypoint: []string{"sh", "-c", fmt.Sprintf("mkdir -p /root/.config/solana/cli && solana-test-validator --rpc-port %s --mint %s", in.Port, in.PublicKey)},
}

c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, err
}
host, err := framework.GetHost(c)
if err != nil {
return nil, err
}

return &Output{
UseCache: true,
Family: "solana",
ContainerName: containerName,
Nodes: []*Node{
{
HostWSUrl: fmt.Sprintf("ws://%s:%s", host, in.WSPort),
HostHTTPUrl: fmt.Sprintf("http://%s:%s", host, in.Port),
DockerInternalWSUrl: fmt.Sprintf("ws://%s:%s", containerName, in.WSPort),
DockerInternalHTTPUrl: fmt.Sprintf("http://%s:%s", containerName, in.Port),
},
},
}, nil
}
Loading

0 comments on commit 67350e9

Please sign in to comment.