Skip to content
Draft
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 chain/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"slices"

"github.com/smartcontractkit/chainlink-deployments-framework/chain/aptos"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/canton"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/sui"
Expand Down Expand Up @@ -135,6 +136,10 @@ func (b BlockChains) TronChains() map[uint64]tron.Chain {
return getChainsByType[tron.Chain, *tron.Chain](b)
}

func (b BlockChains) CantonChains() map[uint64]canton.Chain {
return getChainsByType[canton.Chain, *canton.Chain](b)
}

// ChainSelectorsOption defines a function type for configuring ChainSelectors
type ChainSelectorsOption func(*chainSelectorsOptions)

Expand Down
32 changes: 29 additions & 3 deletions chain/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/smartcontractkit/chainlink-deployments-framework/chain"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/aptos"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/canton"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/sui"
Expand All @@ -25,6 +26,7 @@ var aptosChain1 = aptos.Chain{Selector: chainsel.APTOS_LOCALNET.Selector}
var suiChain1 = sui.Chain{ChainMetadata: sui.ChainMetadata{Selector: chainsel.SUI_LOCALNET.Selector}}
var tonChain1 = ton.Chain{ChainMetadata: ton.ChainMetadata{Selector: chainsel.TON_LOCALNET.Selector}}
var tronChain1 = tron.Chain{ChainMetadata: tron.ChainMetadata{Selector: chainsel.TRON_MAINNET.Selector}}
var cantonChain1 = canton.Chain{Selector: chainsel.CANTON_LOCALNET.Selector}

func TestNewBlockChains(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -162,7 +164,7 @@ func TestBlockChainsAllChains(t *testing.T) {
evmChain1.Selector, evmChain2.Selector,
solanaChain1.Selector, aptosChain1.Selector,
suiChain1.Selector, tonChain1.Selector,
tronChain1.Selector,
tronChain1.Selector, cantonChain1.Selector,
}

assert.Len(t, allChains, len(expectedSelectors))
Expand Down Expand Up @@ -280,6 +282,21 @@ func TestBlockChainsGetters(t *testing.T) {
},
description: "should return all Tron chains",
},
{
name: "CantonChains",
runTest: func(t *testing.T, chains chain.BlockChains) {
t.Helper()
cantonChains := chains.CantonChains()
expectedSelectors := []uint64{cantonChain1.Selector}

assert.Len(t, cantonChains, len(expectedSelectors), "unexpected number of Canton chains")

for _, selector := range expectedSelectors {
_, exists := cantonChains[selector]
assert.True(t, exists, "expected Canton chain with selector %d", selector)
}
},
},
}

// Run tests for both value and pointer chains
Expand Down Expand Up @@ -319,7 +336,7 @@ func TestBlockChainsListChainSelectors(t *testing.T) {
evmChain1.ChainSelector(), evmChain2.ChainSelector(),
solanaChain1.ChainSelector(), aptosChain1.ChainSelector(),
suiChain1.ChainSelector(), tonChain1.ChainSelector(),
tronChain1.ChainSelector(),
tronChain1.ChainSelector(), cantonChain1.ChainSelector(),
},
description: "expected all chain selectors",
},
Expand Down Expand Up @@ -359,6 +376,12 @@ func TestBlockChainsListChainSelectors(t *testing.T) {
expectedIDs: []uint64{tronChain1.Selector},
description: "expected Tron chain selectors",
},
{
name: "with family filter - Canton",
options: []chain.ChainSelectorsOption{chain.WithFamily(chainsel.FamilyCanton)},
expectedIDs: []uint64{cantonChain1.Selector},
description: "expected Canton chain selectors",
},
{
name: "with multiple families",
options: []chain.ChainSelectorsOption{chain.WithFamily(chainsel.FamilyEVM), chain.WithFamily(chainsel.FamilySolana)},
Expand All @@ -370,7 +393,7 @@ func TestBlockChainsListChainSelectors(t *testing.T) {
options: []chain.ChainSelectorsOption{chain.WithChainSelectorsExclusion(
[]uint64{evmChain1.Selector, aptosChain1.Selector}),
},
expectedIDs: []uint64{evmChain2.Selector, solanaChain1.Selector, suiChain1.Selector, tonChain1.Selector, tronChain1.Selector},
expectedIDs: []uint64{evmChain2.Selector, solanaChain1.Selector, suiChain1.Selector, tonChain1.Selector, tronChain1.Selector, cantonChain1.Selector},
description: "expected chain selectors excluding 1 and 4",
},
{
Expand Down Expand Up @@ -405,6 +428,7 @@ func buildBlockChains() chain.BlockChains {
suiChain1.ChainSelector(): suiChain1,
tonChain1.ChainSelector(): tonChain1,
tronChain1.ChainSelector(): tronChain1,
cantonChain1.ChainSelector(): cantonChain1,
})

return chains
Expand All @@ -428,6 +452,8 @@ func buildBlockChainsPointers() chain.BlockChains {
pointerChains[selector] = &c
case tron.Chain:
pointerChains[selector] = &c
case canton.Chain:
pointerChains[selector] = &c
default:
continue // skip unsupported chains
}
Expand Down
36 changes: 36 additions & 0 deletions chain/canton/canton_chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package canton

import (
"context"

chaincommon "github.com/smartcontractkit/chainlink-deployments-framework/chain/internal/common"
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
)

type Chain struct {
Selector uint64

Participants []Participant
}

func (c Chain) ChainSelector() uint64 {
return c.Selector
}

func (c Chain) String() string {
return chaincommon.ChainMetadata{Selector: c.Selector}.String()
}

func (c Chain) Name() string {
return chaincommon.ChainMetadata{Selector: c.Selector}.Name()
}

func (c Chain) Family() string {
return chaincommon.ChainMetadata{Selector: c.Selector}.Family()
}

type Participant struct {
Name string
Endpoints blockchain.CantonParticipantEndpoints
JWT func(ctx context.Context) (string, error)
}
138 changes: 138 additions & 0 deletions chain/canton/provider/ctf_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package provider

import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"sync"
"testing"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/smartcontractkit/freeport"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-deployments-framework/chain"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/canton"
"github.com/smartcontractkit/chainlink-testing-framework/framework"
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
ctfCanton "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain/canton"
)

type CTFChainProviderConfig struct {
NumberOfValidators int

// Required: A sync.Once instance to ensure that the CTF framework only sets up the new
// DefaultNetwork once
Once *sync.Once
}

func (c CTFChainProviderConfig) validate() error {
if c.NumberOfValidators <= 0 {
return errors.New("number of validators must be greater than zero")
}
if c.Once == nil {
return errors.New("sync.Once instance is required")
}

return nil
}

var _ chain.Provider = (*CTFChainProvider)(nil)

type CTFChainProvider struct {
t *testing.T
selector uint64
config CTFChainProviderConfig

chain *canton.Chain
}

func NewCTFChainProvider(t *testing.T, selector uint64, config CTFChainProviderConfig) *CTFChainProvider {
t.Helper()

p := &CTFChainProvider{
t: t,
selector: selector,
config: config,
}

return p
}

func (p CTFChainProvider) Initialize(ctx context.Context) (chain.BlockChain, error) {
if p.chain != nil {
return p.chain, nil // already initialized
}

if err := p.config.validate(); err != nil {
return nil, err
}

// initialize the docker network used by CTF
if err := framework.DefaultNetwork(p.config.Once); err != nil {
return nil, err
}

port := freeport.GetOne(p.t)
fmt.Println("Port for Canton CTF:", port)
input := &blockchain.Input{
Type: blockchain.TypeCanton,
Image: "",
Port: strconv.Itoa(port),
NumberOfCantonValidators: p.config.NumberOfValidators,
}
output, err := blockchain.NewBlockchainNetwork(input)
if err != nil {
p.t.Logf("Error creating Canton blockchain network: %v", err)
freeport.Return([]int{port})
return nil, err
}

// Test HTTP health endpoint
for i, participant := range output.NetworkSpecificData.CantonEndpoints.Participants {
resp, err := http.Get(fmt.Sprintf("%s/health", participant.HTTPHealthCheckURL))
require.NoErrorf(p.t, err, "Error reaching Canton participant %d health endpoint", i+1)
_ = resp.Body.Close()
require.EqualValues(p.t, http.StatusOK, resp.StatusCode, "Unexpected status code from Canton participant %d health endpoint", i+1)
}

p.chain = &canton.Chain{
Selector: p.selector,
Participants: make([]canton.Participant, len(output.NetworkSpecificData.CantonEndpoints.Participants)),
}

for i, participantEndpoints := range output.NetworkSpecificData.CantonEndpoints.Participants {
p.chain.Participants[i] = canton.Participant{
Name: fmt.Sprintf("Participant %v", i+1),
Endpoints: participantEndpoints,
JWT: func(_ context.Context) (string, error) {
return jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
Issuer: "",
Subject: fmt.Sprintf("user-participant%v", i+1),
Audience: []string{ctfCanton.AuthProviderAudience},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
NotBefore: jwt.NewNumericDate(time.Now()),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: "",
}).SignedString([]byte(ctfCanton.AuthProviderSecret))
},
}
}

return p.chain, nil
}

func (p CTFChainProvider) Name() string {
return "Canton CTF Chain Provider"
}

func (p CTFChainProvider) ChainSelector() uint64 {
return p.selector
}

func (p CTFChainProvider) BlockChain() chain.BlockChain {
return p.chain
}
106 changes: 106 additions & 0 deletions chain/canton/provider/ctf_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package provider

import (
"testing"

chainsel "github.com/smartcontractkit/chain-selectors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-deployments-framework/chain/canton"
"github.com/smartcontractkit/chainlink-deployments-framework/chain/internal/testutils"
)

func Test_CTFChainProviderConfig_validate(t *testing.T) {
t.Parallel()

tests := []struct {
name string
config CTFChainProviderConfig
wantErr string
}{
{
name: "valid config",
config: CTFChainProviderConfig{
NumberOfValidators: 4,
Once: testutils.DefaultNetworkOnce,
},
wantErr: "",
},
{
name: "valid config",
config: CTFChainProviderConfig{
NumberOfValidators: 3,
Once: nil,
},
wantErr: "sync.Once instance is required",
},
{
name: "invalid number of validators",
config: CTFChainProviderConfig{
NumberOfValidators: -99,
Once: testutils.DefaultNetworkOnce,
},
wantErr: "number of validators must be greater than zero",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

err := tt.config.validate()
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
})
}
}

func Test_CTFChainProvider_Initialize(t *testing.T) {
t.Parallel()

tests := []struct {
name string
giveSelector uint64
giveConfig CTFChainProviderConfig
wantErr string
}{
{
name: "valid initialization",
giveSelector: chainsel.CANTON_LOCALNET.Selector,
giveConfig: CTFChainProviderConfig{
NumberOfValidators: 5,
Once: testutils.DefaultNetworkOnce,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

provider := NewCTFChainProvider(t, tt.giveSelector, tt.giveConfig)
got, err := provider.Initialize(t.Context())
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
} else {
require.NoError(t, err)
require.NotNil(t, got)

gotChain, ok := got.(*canton.Chain)
require.True(t, ok, "expected chain to be of type *canton.Chain")
assert.Equal(t, tt.giveSelector, gotChain.Selector)
assert.Len(t, gotChain.Participants, tt.giveConfig.NumberOfValidators)
// Test that we can retrieve JWTs for each participant
for _, participant := range gotChain.Participants {
jwt, err := participant.JWT(t.Context())
require.NoError(t, err)
assert.NotEmpty(t, jwt)
}
}
})
}
}
Loading
Loading