Skip to content

Commit f051994

Browse files
authored
Add Sui chain providers (#257)
- Add new providers for the Sui Chain interface: RPC and CTF - Add account generator provider
1 parent 49b797b commit f051994

File tree

10 files changed

+819
-8
lines changed

10 files changed

+819
-8
lines changed

.changeset/all-results-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
Add Sui chain providers for RPC and CTF
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package provider
2+
3+
import (
4+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/sui"
5+
)
6+
7+
// AccountGenerator is an interface for generating Sui accounts.
8+
type AccountGenerator interface {
9+
Generate() (sui.SuiSigner, error)
10+
}
11+
12+
var (
13+
_ AccountGenerator = (*accountGenPrivateKey)(nil)
14+
)
15+
16+
// accountGenPrivateKey is an account generator that creates an account from the private key.
17+
type accountGenPrivateKey struct {
18+
// PrivateKey is the hex formatted private key used to generate the Aptos account.
19+
PrivateKey string
20+
}
21+
22+
// AccountGenPrivateKey creates a new instance of accountGenPrivateKey with the provided private key.
23+
func AccountGenPrivateKey(privateKey string) *accountGenPrivateKey {
24+
return &accountGenPrivateKey{
25+
PrivateKey: privateKey,
26+
}
27+
}
28+
29+
// Generate generates an Sui account from the provided private key. It returns an error if the
30+
// private key string cannot be parsed.
31+
func (g *accountGenPrivateKey) Generate() (sui.SuiSigner, error) {
32+
return sui.NewSignerFromHexPrivateKey(g.PrivateKey)
33+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package provider
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func Test_AccountGenPrivateKey(t *testing.T) {
11+
t.Parallel()
12+
13+
tests := []struct {
14+
name string
15+
givePrivateKey string
16+
wantAddr string
17+
wantErr string
18+
}{
19+
{
20+
name: "valid private key",
21+
givePrivateKey: testPrivateKey,
22+
wantAddr: testAccountAddr,
23+
},
24+
{
25+
name: "invalid private key",
26+
givePrivateKey: "invalid_private_key",
27+
wantErr: "hex private key must be exactly 64 characters",
28+
},
29+
{
30+
name: "empty private key",
31+
givePrivateKey: "",
32+
wantErr: "hex private key must be exactly 64 characters",
33+
},
34+
{
35+
name: "invalid hex private key",
36+
givePrivateKey: "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ",
37+
wantErr: "encoding/hex: invalid byte",
38+
},
39+
{
40+
name: "wrong length private key",
41+
givePrivateKey: "E4FD0E90D32CB98DC6AD64516A421E8C",
42+
wantErr: "hex private key must be exactly 64 characters",
43+
},
44+
}
45+
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
t.Parallel()
49+
50+
gen := AccountGenPrivateKey(tt.givePrivateKey)
51+
account, err := gen.Generate()
52+
if tt.wantErr != "" {
53+
require.Error(t, err)
54+
require.ErrorContains(t, err, tt.wantErr)
55+
} else {
56+
require.NoError(t, err)
57+
assert.NotNil(t, account)
58+
59+
// Verify the account generates the expected address
60+
actualAddr, err := account.GetAddress()
61+
require.NoError(t, err)
62+
63+
assert.Equal(t, tt.wantAddr, actualAddr)
64+
}
65+
})
66+
}
67+
}

chain/sui/provider/const_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package provider
2+
3+
const (
4+
// testPrivateKey is a valid Sui Ed25519 private key for testing purposes (32 bytes, 64 hex chars)
5+
testPrivateKey = "E4FD0E90D32CB98DC6AD64516A421E8C2731870217CDBA64203CEB158A866304"
6+
7+
// testAccountAddr is the expected Sui address derived from testPrivateKey
8+
testAccountAddr = "0xa402ce953053607dffcdfec89406c579c8d8ddb9c90e01b7aa28f5f1538ac289"
9+
)

chain/sui/provider/ctf_provider.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"sync"
8+
"testing"
9+
"time"
10+
11+
"github.com/avast/retry-go/v4"
12+
sui_sdk "github.com/block-vision/sui-go-sdk/sui"
13+
chain_selectors "github.com/smartcontractkit/chain-selectors"
14+
"github.com/smartcontractkit/chainlink-testing-framework/framework"
15+
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
16+
"github.com/stretchr/testify/require"
17+
"github.com/testcontainers/testcontainers-go"
18+
19+
"github.com/smartcontractkit/chainlink-deployments-framework/chain"
20+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/sui"
21+
)
22+
23+
// CTFChainProviderConfig holds the configuration to initialize the CTFChainProvider.
24+
type CTFChainProviderConfig struct {
25+
// Required: A generator for the deployer signer account. Use AccountGenPrivateKey to
26+
// create a deployer signer from a hex private key.
27+
DeployerSignerGen AccountGenerator
28+
29+
// Required: A sync.Once instance to ensure that the CTF framework only sets up the new
30+
// DefaultNetwork once
31+
Once *sync.Once
32+
}
33+
34+
// validate checks if the CTFChainProviderConfig is valid.
35+
func (c CTFChainProviderConfig) validate() error {
36+
if c.DeployerSignerGen == nil {
37+
return errors.New("deployer signer generator is required")
38+
}
39+
40+
if c.Once == nil {
41+
return errors.New("sync.Once instance is required")
42+
}
43+
44+
return nil
45+
}
46+
47+
var _ chain.Provider = (*CTFChainProvider)(nil)
48+
49+
// CTFChainProvider manages a Sui chain instance running inside a Chainlink Testing Framework (CTF) Docker container.
50+
//
51+
// This provider requires Docker to be installed and operational. Spinning up a new container can be slow,
52+
// so it is recommended to initialize the provider only once per test suite or parent test to optimize performance.
53+
type CTFChainProvider struct {
54+
t *testing.T
55+
selector uint64
56+
config CTFChainProviderConfig
57+
58+
chain *sui.Chain
59+
}
60+
61+
// NewCTFChainProvider creates a new CTFChainProvider with the given selector and configuration.
62+
func NewCTFChainProvider(
63+
t *testing.T, selector uint64, config CTFChainProviderConfig,
64+
) *CTFChainProvider {
65+
t.Helper()
66+
67+
p := &CTFChainProvider{
68+
t: t,
69+
selector: selector,
70+
config: config,
71+
}
72+
73+
return p
74+
}
75+
76+
// Initialize sets up the Sui chain by validating the configuration, starting a CTF container,
77+
// generating a deployer signer account, and constructing the chain instance.
78+
func (p *CTFChainProvider) Initialize(_ context.Context) (chain.BlockChain, error) {
79+
if p.chain != nil {
80+
return *p.chain, nil // Already initialized
81+
}
82+
83+
if err := p.config.validate(); err != nil {
84+
return nil, fmt.Errorf("failed to validate provider config: %w", err)
85+
}
86+
87+
// Generate the deployer account
88+
deployerSigner, err := p.config.DeployerSignerGen.Generate()
89+
if err != nil {
90+
return nil, fmt.Errorf("failed to generate deployer account: %w", err)
91+
}
92+
93+
// Get the Sui Chain ID
94+
chainID, err := chain_selectors.GetChainIDFromSelector(p.selector)
95+
if err != nil {
96+
return nil, fmt.Errorf("failed to get chain ID from selector %d: %w", p.selector, err)
97+
}
98+
99+
// Start the CTF Container
100+
url, client := p.startContainer(chainID, deployerSigner)
101+
102+
// Construct the chain
103+
p.chain = &sui.Chain{
104+
ChainMetadata: sui.ChainMetadata{
105+
Selector: p.selector,
106+
},
107+
Client: client,
108+
Signer: deployerSigner,
109+
URL: url,
110+
// TODO: Implement ConfirmTransaction when available
111+
}
112+
113+
return *p.chain, nil
114+
}
115+
116+
// Name returns the name of the CTFChainProvider.
117+
func (*CTFChainProvider) Name() string {
118+
return "Sui CTF Chain Provider"
119+
}
120+
121+
// ChainSelector returns the chain selector of the Sui chain managed by this provider.
122+
func (p *CTFChainProvider) ChainSelector() uint64 {
123+
return p.selector
124+
}
125+
126+
// BlockChain returns the Sui chain instance managed by this provider. You must call Initialize
127+
// before using this method to ensure the chain is properly set up.
128+
func (p *CTFChainProvider) BlockChain() chain.BlockChain {
129+
return *p.chain
130+
}
131+
132+
// startContainer starts a CTF container for the Sui chain with the given chain ID and deployer account.
133+
// It returns the URL of the Sui node and the client to interact with it.
134+
func (p *CTFChainProvider) startContainer(
135+
chainID string, account sui.SuiSigner,
136+
) (string, sui_sdk.ISuiAPI) {
137+
var (
138+
attempts = uint(10)
139+
url string
140+
containerName string
141+
)
142+
143+
// initialize the docker network used by CTF
144+
err := framework.DefaultNetwork(p.config.Once)
145+
require.NoError(p.t, err)
146+
147+
// Get address from signer
148+
address, err := account.GetAddress()
149+
require.NoError(p.t, err)
150+
151+
type containerResult struct {
152+
url string
153+
containerName string
154+
}
155+
156+
result, err := retry.DoWithData(func() (containerResult, error) {
157+
// NOTE: Sui blockchain containers use hardcoded ports (9000/9123) and ignore the Port field
158+
input := &blockchain.Input{
159+
Image: "", // filled out by defaultSui function
160+
Type: blockchain.TypeSui,
161+
ChainID: chainID,
162+
PublicKey: address,
163+
// Port field is ignored by Sui containers - they always use ports 9000/9123
164+
}
165+
166+
output, rerr := blockchain.NewBlockchainNetwork(input)
167+
if rerr != nil {
168+
return containerResult{}, rerr
169+
}
170+
171+
testcontainers.CleanupContainer(p.t, output.Container)
172+
173+
return containerResult{
174+
url: output.Nodes[0].ExternalHTTPUrl + "/v1",
175+
containerName: output.ContainerName,
176+
}, nil
177+
},
178+
retry.Context(p.t.Context()),
179+
retry.Attempts(attempts),
180+
retry.Delay(1*time.Second),
181+
retry.DelayType(retry.FixedDelay),
182+
)
183+
require.NoError(p.t, err)
184+
185+
url = result.url
186+
containerName = result.containerName
187+
188+
client := sui_sdk.NewSuiClient(url)
189+
190+
var ready bool
191+
for i := range 30 {
192+
time.Sleep(time.Second)
193+
// TODO: Add appropriate readiness check when available
194+
p.t.Logf("Sui client ready check (attempt %d)\n", i+1)
195+
ready = true
196+
197+
break
198+
}
199+
require.True(p.t, ready, "Sui network not ready")
200+
201+
dc, err := framework.NewDockerClient()
202+
require.NoError(p.t, err)
203+
204+
_, err = dc.ExecContainer(containerName, []string{
205+
"sui", "client", "faucet",
206+
"--address", address,
207+
})
208+
require.NoError(p.t, err)
209+
210+
return url, client
211+
}

0 commit comments

Comments
 (0)