Skip to content

Commit 67350e9

Browse files
feat: solana blockchain support (#1509)
Solana support
1 parent c0699a9 commit 67350e9

File tree

11 files changed

+334
-180
lines changed

11 files changed

+334
-180
lines changed

.github/workflows/framework-golden-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ jobs:
2222
config: smoke.toml
2323
count: 1
2424
timeout: 10m
25+
- name: TestSolanaSmoke
26+
config: smoke_solana.toml
27+
count: 1
28+
timeout: 10m
2529
- name: TestUpgrade
2630
config: upgrade.toml
2731
count: 1

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
- [Components](framework/components/overview.md)
3737
- [Blockchains](framework/components/blockchains/overview.md)
3838
- [EVM](framework/components/blockchains/evm.md)
39+
- [Solana](framework/components/blockchains/solana.md)
3940
- [Optimism Stack]()
4041
- [Arbitrum Stack]()
4142
- [Chainlink](framework/components/chainlink.md)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Solana Blockchain Client
2+
3+
Since `Solana` doesn't have official image for `arm64` we built it, images we use are:
4+
```
5+
amd64 solanalabs/solana:v1.18.26 - used in CI
6+
arm64 f4hrenh9it/solana:latest - used locally
7+
```
8+
9+
## Configuration
10+
```toml
11+
[blockchain_a]
12+
type = "solana"
13+
# public key for mint
14+
public_key = "9n1pyVGGo6V4mpiSDMVay5As9NurEkY283wwRk1Kto2C"
15+
# contracts directory, programs
16+
contracts_dir = "."
17+
# optional, in case you need some custom image
18+
# image = "solanalabs/solana:v1.18.26"
19+
```
20+
21+
## Usage
22+
```golang
23+
package examples
24+
25+
import (
26+
"context"
27+
"fmt"
28+
"github.com/blocto/solana-go-sdk/client"
29+
"github.com/smartcontractkit/chainlink-testing-framework/framework"
30+
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
31+
"github.com/stretchr/testify/require"
32+
"testing"
33+
)
34+
35+
type CfgSolana struct {
36+
BlockchainA *blockchain.Input `toml:"blockchain_a" validate:"required"`
37+
}
38+
39+
func TestSolanaSmoke(t *testing.T) {
40+
in, err := framework.Load[CfgSolana](t)
41+
require.NoError(t, err)
42+
43+
bc, err := blockchain.NewBlockchainNetwork(in.BlockchainA)
44+
require.NoError(t, err)
45+
46+
t.Run("test something", func(t *testing.T) {
47+
// use internal URL to connect chainlink nodes
48+
_ = bc.Nodes[0].DockerInternalHTTPUrl
49+
// use host URL to deploy contracts
50+
c := client.NewClient(bc.Nodes[0].HostHTTPUrl)
51+
latestSlot, err := c.GetSlotWithConfig(context.Background(), client.GetSlotConfig{Commitment: "processed"})
52+
require.NoError(t, err)
53+
fmt.Printf("Latest slot: %v\n", latestSlot)
54+
})
55+
}
56+
```
57+
58+
## Test Private Keys
59+
60+
```
61+
Public: 9n1pyVGGo6V4mpiSDMVay5As9NurEkY283wwRk1Kto2C
62+
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]
63+
```
64+

framework/.changeset/v0.4.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add basic Solana network support

framework/components/blockchain/blockchain.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@ import (
66

77
// Input is a blockchain network configuration params
88
type Input struct {
9-
Type string `toml:"type" validate:"required,oneof=anvil geth besu" envconfig:"net_type"`
10-
Image string `toml:"image"`
11-
PullImage bool `toml:"pull_image"`
12-
Port string `toml:"port"`
9+
// Common EVM fields
10+
Type string `toml:"type" validate:"required,oneof=anvil geth besu solana" envconfig:"net_type"`
11+
Image string `toml:"image"`
12+
PullImage bool `toml:"pull_image"`
13+
Port string `toml:"port"`
14+
// Not applicable to Solana, ws port for Solana is +1 of port
1315
WSPort string `toml:"port_ws"`
1416
ChainID string `toml:"chain_id"`
1517
DockerCmdParamsOverrides []string `toml:"docker_cmd_params"`
1618
Out *Output `toml:"out"`
19+
20+
// Solana fields
21+
// publickey to mint when solana-test-validator starts
22+
PublicKey string `toml:"public_key"`
23+
ContractsDir string `toml:"contracts_dir"`
1724
}
1825

1926
// Output is a blockchain network output, ChainID and one or more nodes that forms the network
@@ -49,6 +56,8 @@ func NewBlockchainNetwork(in *Input) (*Output, error) {
4956
out, err = newGeth(in)
5057
case "besu":
5158
out, err = newBesu(in)
59+
case "solana":
60+
out, err = newSolana(in)
5261
default:
5362
return nil, fmt.Errorf("blockchain type is not supported or empty, must be 'anvil' or 'geth'")
5463
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package blockchain
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strconv"
9+
"time"
10+
11+
"github.com/docker/docker/api/types/container"
12+
"github.com/docker/docker/api/types/mount"
13+
"github.com/docker/go-connections/nat"
14+
"github.com/testcontainers/testcontainers-go"
15+
"github.com/testcontainers/testcontainers-go/wait"
16+
17+
"github.com/smartcontractkit/chainlink-testing-framework/framework"
18+
)
19+
20+
var configYmlRaw = `
21+
json_rpc_url: http://0.0.0.0:%s
22+
websocket_url: ws://0.0.0.0:%s
23+
keypair_path: /root/.config/solana/cli/id.json
24+
address_labels:
25+
"11111111111111111111111111111111": ""
26+
commitment: finalized
27+
`
28+
29+
var idJSONRaw = `
30+
[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]
31+
`
32+
33+
func defaultSolana(in *Input) {
34+
ci := os.Getenv("CI") == "true"
35+
if in.Image == "" && !ci {
36+
in.Image = "f4hrenh9it/solana"
37+
}
38+
if in.Image == "" && ci {
39+
in.Image = "solanalabs/solana:v1.18.26"
40+
}
41+
if in.Port == "" {
42+
in.Port = "8545"
43+
}
44+
}
45+
46+
func newSolana(in *Input) (*Output, error) {
47+
defaultSolana(in)
48+
ctx := context.Background()
49+
containerName := framework.DefaultTCName("blockchain-node")
50+
// Solana do not allow to set ws port, it just uses --rpc-port=N and sets WS as N+1 automatically
51+
bindPort := fmt.Sprintf("%s/tcp", in.Port)
52+
pp, err := strconv.Atoi(in.Port)
53+
if err != nil {
54+
return nil, fmt.Errorf("in.Port is not a number")
55+
}
56+
in.WSPort = strconv.Itoa(pp + 1)
57+
wsBindPort := fmt.Sprintf("%s/tcp", in.WSPort)
58+
59+
configYml, err := os.CreateTemp("", "config.yml")
60+
if err != nil {
61+
return nil, err
62+
}
63+
configYmlRaw = fmt.Sprintf(configYmlRaw, in.Port, in.WSPort)
64+
_, err = configYml.WriteString(configYmlRaw)
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
idJSON, err := os.CreateTemp("", "id.json")
70+
if err != nil {
71+
return nil, err
72+
}
73+
_, err = idJSON.WriteString(idJSONRaw)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
contractsDir, err := filepath.Abs(in.ContractsDir)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
req := testcontainers.ContainerRequest{
84+
AlwaysPullImage: in.PullImage,
85+
Image: in.Image,
86+
Labels: framework.DefaultTCLabels(),
87+
Name: containerName,
88+
ExposedPorts: []string{bindPort, wsBindPort},
89+
Networks: []string{framework.DefaultNetworkName},
90+
NetworkAliases: map[string][]string{
91+
framework.DefaultNetworkName: {containerName},
92+
},
93+
WaitingFor: wait.ForLog("Processed Slot: 1").
94+
WithStartupTimeout(30 * time.Second).
95+
WithPollInterval(100 * time.Millisecond),
96+
HostConfigModifier: func(h *container.HostConfig) {
97+
h.PortBindings = nat.PortMap{
98+
nat.Port(bindPort): []nat.PortBinding{
99+
{
100+
HostIP: "0.0.0.0",
101+
HostPort: bindPort,
102+
},
103+
},
104+
nat.Port(wsBindPort): []nat.PortBinding{
105+
{
106+
HostIP: "0.0.0.0",
107+
HostPort: wsBindPort,
108+
},
109+
},
110+
}
111+
h.Mounts = append(h.Mounts, mount.Mount{
112+
Type: mount.TypeBind,
113+
Source: contractsDir,
114+
Target: "/programs",
115+
ReadOnly: false,
116+
})
117+
},
118+
Files: []testcontainers.ContainerFile{
119+
{
120+
HostFilePath: configYml.Name(),
121+
ContainerFilePath: "/root/.config/solana/cli/config.yml",
122+
FileMode: 0644,
123+
},
124+
{
125+
HostFilePath: idJSON.Name(),
126+
ContainerFilePath: "/root/.config/solana/cli/id.json",
127+
FileMode: 0644,
128+
},
129+
},
130+
Entrypoint: []string{"sh", "-c", fmt.Sprintf("mkdir -p /root/.config/solana/cli && solana-test-validator --rpc-port %s --mint %s", in.Port, in.PublicKey)},
131+
}
132+
133+
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
134+
ContainerRequest: req,
135+
Started: true,
136+
})
137+
if err != nil {
138+
return nil, err
139+
}
140+
host, err := framework.GetHost(c)
141+
if err != nil {
142+
return nil, err
143+
}
144+
145+
return &Output{
146+
UseCache: true,
147+
Family: "solana",
148+
ContainerName: containerName,
149+
Nodes: []*Node{
150+
{
151+
HostWSUrl: fmt.Sprintf("ws://%s:%s", host, in.WSPort),
152+
HostHTTPUrl: fmt.Sprintf("http://%s:%s", host, in.Port),
153+
DockerInternalWSUrl: fmt.Sprintf("ws://%s:%s", containerName, in.WSPort),
154+
DockerInternalHTTPUrl: fmt.Sprintf("http://%s:%s", containerName, in.Port),
155+
},
156+
},
157+
}, nil
158+
}

0 commit comments

Comments
 (0)