Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-crews-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": patch
---

fixes for sui provider
51 changes: 34 additions & 17 deletions chain/sui/provider/ctf_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import (
"testing"
"time"

"github.com/go-resty/resty/v2"

"github.com/avast/retry-go/v4"
"github.com/block-vision/sui-go-sdk/models"
sui_sdk "github.com/block-vision/sui-go-sdk/sui"
chainsel "github.com/smartcontractkit/chain-selectors"
"github.com/smartcontractkit/chainlink-testing-framework/framework"
Expand Down Expand Up @@ -108,16 +111,17 @@ func (p *CTFChainProvider) Initialize(_ context.Context) (chain.BlockChain, erro
}

// Start the CTF Container
url, client := p.startContainer(chainID, deployerSigner)
url, faucetUrl, client := p.startContainer(chainID, deployerSigner)

// Construct the chain
p.chain = &sui.Chain{
ChainMetadata: sui.ChainMetadata{
Selector: p.selector,
},
Client: client,
Signer: deployerSigner,
URL: url,
Client: client,
Signer: deployerSigner,
URL: url,
FaucetURL: faucetUrl,
// TODO: Implement ConfirmTransaction when available
}

Expand All @@ -144,11 +148,11 @@ func (p *CTFChainProvider) BlockChain() chain.BlockChain {
// It returns the URL of the Sui node and the client to interact with it.
func (p *CTFChainProvider) startContainer(
chainID string, account sui.SuiSigner,
) (string, sui_sdk.ISuiAPI) {
) (string, string, sui_sdk.ISuiAPI) {
var (
attempts = uint(10)
url string
containerName string
attempts = uint(10)
url string
fauceturl string
)

// initialize the docker network used by CTF
Expand All @@ -161,6 +165,7 @@ func (p *CTFChainProvider) startContainer(

type containerResult struct {
url string
faucetPort string
containerName string
}

Expand Down Expand Up @@ -204,13 +209,15 @@ func (p *CTFChainProvider) startContainer(
if rerr != nil {
// Return the ports to freeport to avoid leaking them during retries
freeport.Return([]int{port, faucetPort})

return containerResult{}, rerr
}

testcontainers.CleanupContainer(p.t, output.Container)

return containerResult{
url: output.Nodes[0].ExternalHTTPUrl + "/v1",
url: output.Nodes[0].ExternalHTTPUrl,
faucetPort: input.FaucetPort,
containerName: output.ContainerName,
}, nil
},
Expand All @@ -225,7 +232,7 @@ func (p *CTFChainProvider) startContainer(
require.NoError(p.t, err, "Failed to start CTF Sui container after %d attempts", attempts)

url = result.url
containerName = result.containerName
fauceturl = fmt.Sprintf("http://%s:%s", "127.0.0.1", result.faucetPort)

client := sui_sdk.NewSuiClient(url)

Expand All @@ -240,14 +247,24 @@ func (p *CTFChainProvider) startContainer(
}
require.True(p.t, ready, "Sui network not ready")

dc, err := framework.NewDockerClient()
err = fundAccount(fauceturl, address)
require.NoError(p.t, err)

_, err = dc.ExecContainer(containerName, []string{
"sui", "client", "faucet",
"--address", address,
})
require.NoError(p.t, err)
return url, fauceturl, client
}

return url, client
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
}
64 changes: 49 additions & 15 deletions chain/sui/signer.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package sui

import (
"crypto/ed25519"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"

"github.com/block-vision/sui-go-sdk/constant"
"github.com/block-vision/sui-go-sdk/signer"
"golang.org/x/crypto/blake2b"
)

// TODO: Everything in this file should come from chainlink-sui when available
Expand Down Expand Up @@ -46,24 +46,58 @@ func NewSignerFromHexPrivateKey(hexPrivateKey string) (SuiSigner, error) {
}

func (s *suiSigner) Sign(message []byte) ([]string, error) {
if s.signer == nil {
return nil, errors.New("signer is nil")
}
// Add intent scope for transaction data (0x00, 0x00, 0x00)
intentMessage := append([]byte{0x00, 0x00, 0x00}, message...)

// Sign the message as a transaction message
b64Message := base64.StdEncoding.EncodeToString(message)
signedMsg, err := s.signer.SignMessage(b64Message, constant.TransactionDataIntentScope)
if err != nil {
return nil, fmt.Errorf("failed to sign message: %w", err)
}
// Hash the message with blake2b
hash := blake2b.Sum256(intentMessage)

// Sign the hash
signature := ed25519.Sign(s.signer.PriKey, hash[:])

// Get public key
publicKey := s.signer.PriKey.Public().(ed25519.PublicKey)

// Create serialized signature: flag + signature + pubkey
serializedSig := make([]byte, 1+len(signature)+len(publicKey))
serializedSig[0] = 0x00 // Ed25519 flag
copy(serializedSig[1:], signature)
copy(serializedSig[1+len(signature):], publicKey)

// Encode to base64
encoded := base64.StdEncoding.EncodeToString(serializedSig)

return []string{signedMsg.Signature}, nil
return []string{encoded}, nil
}

func (s *suiSigner) GetAddress() (string, error) {
if s.signer == nil {
return "", errors.New("signer is nil")
publicKey := s.signer.PriKey.Public().(ed25519.PublicKey)

// For Ed25519, the signature scheme is 0x00
const signatureScheme = 0x00

// Create the data to hash: signature scheme byte || public key
data := append([]byte{signatureScheme}, publicKey...)

// Hash using Blake2b-256
hash := blake2b.Sum256(data)

// The Sui address is the hex representation of the hash
return "0x" + hex.EncodeToString(hash[:]), nil
}

// PublicKeyBytes extracts the raw 32-byte ed25519 public key from a SuiSigner.
func PublicKeyBytes(s SuiSigner) ([]byte, error) {
impl, ok := s.(*suiSigner)
if !ok {
return nil, fmt.Errorf("unsupported signer type %T", s)
}
priv := []byte(impl.signer.PriKey)
if len(priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("unexpected ed25519 key length: %d", len(priv))
}
pub := make([]byte, ed25519.PublicKeySize)
copy(pub, priv[32:]) // last 32 bytes are the pubkey

return s.signer.Address, nil
return pub, nil
}
8 changes: 5 additions & 3 deletions chain/sui/sui_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ type ChainMetadata = common.ChainMetadata
// Chain represents an Sui chain.
type Chain struct {
ChainMetadata
Client sui.ISuiAPI
Signer SuiSigner
URL string
Client sui.ISuiAPI
Signer SuiSigner
URL string
FaucetURL string

// TODO: Implement ConfirmTransaction. Current tooling relies on node local execution
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ require (
github.com/xssnick/tonutils-go v1.13.0
github.com/zksync-sdk/zksync2-go v1.1.1-0.20250620124214-2c742ee399c6
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.40.0
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc
golang.org/x/oauth2 v0.30.0
google.golang.org/grpc v1.74.2
Expand Down Expand Up @@ -280,7 +281,6 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
Expand Down
Loading