Skip to content

Commit

Permalink
Sui network support (#1566)
Browse files Browse the repository at this point in the history
Sui network support
  • Loading branch information
skudasov authored Jan 16, 2025
1 parent 5a76b26 commit 4380c2a
Show file tree
Hide file tree
Showing 14 changed files with 347 additions and 7 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: TestSuiSmoke
config: smoke_sui.toml
count: 1
timeout: 10m
- name: TestAptosSmoke
config: smoke_aptos.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 @@ -38,6 +38,7 @@
- [EVM](framework/components/blockchains/evm.md)
- [Solana](framework/components/blockchains/solana.md)
- [Aptos](framework/components/blockchains/aptos.md)
- [Sui](framework/components/blockchains/sui.md)
- [Optimism Stack]()
- [Arbitrum Stack]()
- [Chainlink](framework/components/chainlink.md)
Expand Down
78 changes: 78 additions & 0 deletions book/src/framework/components/blockchains/sui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Sui Blockchain Client

API is available on [localhost:9000](http://localhost:9000)

## Configuration

```toml
[blockchain_a]
type = "sui"
image = "mysten/sui-tools:mainnet" # if omitted default is mysten/sui-tools:devnet
contracts_dir = "$your_dir"
```

## Usage

```golang
package examples

import (
"context"
"fmt"
"github.com/block-vision/sui-go-sdk/models"
"github.com/block-vision/sui-go-sdk/signer"
"github.com/block-vision/sui-go-sdk/sui"
"github.com/smartcontractkit/chainlink-testing-framework/framework"
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
"github.com/stretchr/testify/require"
"testing"
)

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

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

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

// network is already funded, here are the keys
_ = bc.NetworkSpecificData.SuiAccount.Mnemonic
_ = bc.NetworkSpecificData.SuiAccount.PublicBase64Key
_ = bc.NetworkSpecificData.SuiAccount.SuiAddress

// execute any additional commands, to deploy contracts or set up
_, err = framework.ExecContainer(bc.ContainerName, []string{"ls", "-lah"})
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 interact
_ = bc.Nodes[0].HostHTTPUrl

cli := sui.NewSuiClient(bc.Nodes[0].HostHTTPUrl)

signerAccount, err := signer.NewSignertWithMnemonic(bc.NetworkSpecificData.SuiAccount.Mnemonic)
require.NoError(t, err)
rsp, err := cli.SuiXGetAllBalance(context.Background(), models.SuiXGetAllBalanceRequest{
Owner: signerAccount.Address,
})
require.NoError(t, err)
fmt.Printf("My funds: %v\n", rsp)
})
}
```

## Test Private Keys

Since Sui doesn't have official local development chain we are using real node and generating mnemonic at start then funding that account through internal faucet, see
```
// network is already funded, here are the keys
_ = bc.NetworkSpecificData.SuiAccount.Mnemonic
_ = bc.NetworkSpecificData.SuiAccount.PublicBase64Key
_ = bc.NetworkSpecificData.SuiAccount.SuiAddress
```
1 change: 1 addition & 0 deletions framework/.changeset/v0.4.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add Sui network support
21 changes: 14 additions & 7 deletions framework/components/blockchain/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
// Input is a blockchain network configuration params
type Input struct {
// Common EVM fields
Type string `toml:"type" validate:"required,oneof=anvil geth besu solana aptos" envconfig:"net_type"`
Type string `toml:"type" validate:"required,oneof=anvil geth besu solana aptos sui" envconfig:"net_type"`
Image string `toml:"image"`
PullImage bool `toml:"pull_image"`
Port string `toml:"port"`
Expand All @@ -30,12 +30,17 @@ type Input struct {

// Output is a blockchain network output, ChainID and one or more nodes that forms the network
type Output struct {
UseCache bool `toml:"use_cache"`
Family string `toml:"family"`
ContainerName string `toml:"container_name"`
Container testcontainers.Container `toml:"-"`
ChainID string `toml:"chain_id"`
Nodes []*Node `toml:"nodes"`
UseCache bool `toml:"use_cache"`
Family string `toml:"family"`
ContainerName string `toml:"container_name"`
NetworkSpecificData *NetworkSpecificData `toml:"network_specific_data"`
Container testcontainers.Container `toml:"-"`
ChainID string `toml:"chain_id"`
Nodes []*Node `toml:"nodes"`
}

type NetworkSpecificData struct {
SuiAccount *SuiWalletInfo
}

// Node represents blockchain node output, URLs required for connection locally and inside docker network
Expand Down Expand Up @@ -64,6 +69,8 @@ func NewBlockchainNetwork(in *Input) (*Output, error) {
out, err = newSolana(in)
case "aptos":
out, err = newAptos(in)
case "sui":
out, err = newSui(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/sui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package blockchain

import (
"context"
"encoding/json"
"fmt"
"github.com/block-vision/sui-go-sdk/models"
"github.com/docker/docker/api/types/container"
"github.com/go-resty/resty/v2"
"github.com/smartcontractkit/chainlink-testing-framework/framework"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"path/filepath"
"strings"
"time"
)

const (
DefaultFaucetPort = "9123/tcp"
DefaultFaucetPortNum = "9123"
DefaultSuiNodePort = "9000"
)

// SuiWalletInfo info about Sui account/wallet
type SuiWalletInfo struct {
Alias *string `json:"alias"` // Alias key name, usually "null"
Flag int `json:"flag"` // Flag is an integer
KeyScheme string `json:"keyScheme"` // Key scheme is a string
Mnemonic string `json:"mnemonic"` // Mnemonic is a string
PeerId string `json:"peerId"` // Peer ID is a string
PublicBase64Key string `json:"publicBase64Key"` // Public key in Base64 format
SuiAddress string `json:"suiAddress"` // Sui address is a 0x prefixed hex string
}

// funds provided key using local faucet
// we can't use the best client available - block-vision/sui-go-sdk for that, since some versions have old API and it is hardcoded
// https://github.com/block-vision/sui-go-sdk/blob/main/sui/faucet_api.go#L16
func fundAccount(url string, address string) error {
r := resty.New().SetBaseURL(url)
b := &models.FaucetRequest{
FixedAmountRequest: &models.FaucetFixedAmountRequest{
Recipient: address,
},
}
resp, err := r.R().SetBody(b).SetHeader("Content-Type", "application/json").Post("/gas")
if err != nil {
return err
}
framework.L.Info().Any("Resp", resp).Msg("Address is funded!")
return nil
}

// generateKeyData generates a wallet and returns all the data
func generateKeyData(containerName string, keyCipherType string) (*SuiWalletInfo, error) {
cmdStr := []string{"sui", "keytool", "generate", keyCipherType, "--json"}
keyOut, err := framework.ExecContainer(containerName, cmdStr)
if err != nil {
return nil, err
}
// formatted JSON with, no plain --json version, remove special symbols
cleanKey := strings.ReplaceAll(keyOut, "\x00", "")
cleanKey = strings.ReplaceAll(cleanKey, "\x01", "")
cleanKey = strings.ReplaceAll(cleanKey, "\x02", "")
cleanKey = strings.ReplaceAll(cleanKey, "\n", "")
cleanKey = "{" + cleanKey[2:]
var key *SuiWalletInfo
if err := json.Unmarshal([]byte(cleanKey), &key); err != nil {
return nil, err
}
framework.L.Info().Interface("Key", key).Msg("Test key")
return key, nil
}

func defaultSui(in *Input) {
if in.Image == "" {
in.Image = "mysten/sui-tools:devnet"
}
if in.Port != "" {
framework.L.Warn().Msgf("'port' field is set but only default port can be used: %s", DefaultSuiNodePort)
}
in.Port = DefaultSuiNodePort
}

func newSui(in *Input) (*Output, error) {
defaultSui(in)
ctx := context.Background()
containerName := framework.DefaultTCName("blockchain-node")

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

bindPort := fmt.Sprintf("%s/tcp", in.Port)

req := testcontainers.ContainerRequest{
Image: in.Image,
ExposedPorts: []string{in.Port, DefaultFaucetPort},
Name: containerName,
Labels: framework.DefaultTCLabels(),
Networks: []string{framework.DefaultNetworkName},
NetworkAliases: map[string][]string{
framework.DefaultNetworkName: {containerName},
},
HostConfigModifier: func(h *container.HostConfig) {
h.PortBindings = framework.MapTheSamePort(bindPort, DefaultFaucetPort)
},
ImagePlatform: "linux/amd64",
Env: map[string]string{
"RUST_LOG": "off,sui_node=info",
},
Cmd: []string{
"sui",
"start",
"--force-regenesis",
"--with-faucet",
},
Files: []testcontainers.ContainerFile{
{
HostFilePath: absPath,
ContainerFilePath: "/",
},
},
// we need faucet for funding
WaitingFor: wait.ForListeningPort(DefaultFaucetPort).WithStartupTimeout(10 * time.Second).WithPollInterval(200 * time.Millisecond),
}

c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, err
}
host, err := c.Host(ctx)
if err != nil {
return nil, err
}
suiAccount, err := generateKeyData(containerName, "ed25519")
if err != nil {
return nil, err
}
if err := fundAccount(fmt.Sprintf("http://%s:%s", "127.0.0.1", DefaultFaucetPortNum), suiAccount.SuiAddress); err != nil {
return nil, err
}
return &Output{
UseCache: true,
Family: "sui",
ContainerName: containerName,
NetworkSpecificData: &NetworkSpecificData{SuiAccount: suiAccount},
Nodes: []*Node{
{
HostHTTPUrl: fmt.Sprintf("http://%s:%s", host, in.Port),
DockerInternalHTTPUrl: fmt.Sprintf("http://%s:%s", containerName, in.Port),
},
},
}, nil
}
5 changes: 5 additions & 0 deletions framework/examples/myproject/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ replace (
)

require (
github.com/block-vision/sui-go-sdk v1.0.6
github.com/blocto/solana-go-sdk v1.30.0
github.com/ethereum/go-ethereum v1.14.11
github.com/go-resty/resty/v2 v2.15.3
Expand Down Expand Up @@ -221,9 +222,13 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/supranational/blst v0.3.13 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
Expand Down
8 changes: 8 additions & 0 deletions framework/examples/myproject/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/block-vision/sui-go-sdk v1.0.6 h1:FysCc4TJC8v4BEBbCjJPDR4iR5eKqJT1dxGwsT67etg=
github.com/block-vision/sui-go-sdk v1.0.6/go.mod h1:FyK1vGE8lWm9QA1fdQpf1agfXQSMbPT8AV1BICgx6d8=
github.com/blocto/solana-go-sdk v1.30.0 h1:GEh4GDjYk1lMhV/hqJDCyuDeCuc5dianbN33yxL88NU=
github.com/blocto/solana-go-sdk v1.30.0/go.mod h1:Xoyhhb3hrGpEQ5rJps5a3OgMwDpmEhrd9bgzFKkkwMs=
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
Expand Down Expand Up @@ -899,7 +901,13 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo=
github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
Expand Down
3 changes: 3 additions & 0 deletions framework/examples/myproject/smoke_sui.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[blockchain_a]
type = "sui"
image = "mysten/sui-tools:mainnet"
Loading

0 comments on commit 4380c2a

Please sign in to comment.