diff --git a/.changeset/rich-pumas-cheat.md b/.changeset/rich-pumas-cheat.md new file mode 100644 index 00000000..00024c4a --- /dev/null +++ b/.changeset/rich-pumas-cheat.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": patch +--- + +add Stellar support for mcms adapters, RPC provider, chain config, and blockchain diff --git a/.github/workflows/pull-request-main.yml b/.github/workflows/pull-request-main.yml index 5f121e1d..3ee96c5a 100644 --- a/.github/workflows/pull-request-main.yml +++ b/.github/workflows/pull-request-main.yml @@ -117,6 +117,22 @@ jobs: use-go-cache: true artifact-name: provider-tests-tron + ci-test-provider-stellar: + name: Provider Tests - Stellar + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + id-token: write + contents: read + actions: read + steps: + - name: Build and test stellar provider packages + uses: smartcontractkit/.github/actions/ci-test-go@dfcba48f05933158428bce867d790e3d5a9baa6b # ci-test-go@1.1.0 + with: + go-test-cmd: go test -race -coverprofile=coverage.txt ./chain/stellar/provider/... + use-go-cache: true + artifact-name: provider-tests-stellar + ci-test-provider-fast: name: Provider Tests - Others runs-on: ubuntu-latest @@ -130,8 +146,8 @@ jobs: uses: smartcontractkit/.github/actions/ci-test-go@dfcba48f05933158428bce867d790e3d5a9baa6b # ci-test-go@1.1.0 with: # -p 2 -parallel 3 = 2 packages, 3 tests max = 6 containers max - # Run all provider tests EXCEPT slow ones (aptos, canton, ton, tron) which have dedicated jobs - go-test-cmd: go test -race -p 2 -parallel 3 -coverprofile=coverage.txt $(go list ./... | grep '/provider' | grep -E -v '/(aptos|canton|ton|tron)/provider') + # Run all provider tests EXCEPT slow ones (aptos, canton, ton, tron, stellar) which have dedicated jobs + go-test-cmd: go test -race -p 2 -parallel 3 -coverprofile=coverage.txt $(go list ./... | grep '/provider' | grep -E -v '/(aptos|canton|ton|tron|stellar)/provider') use-go-cache: true artifact-name: provider-tests-others @@ -203,7 +219,7 @@ jobs: name: Sonar Scan if: github.event_name == 'pull_request' runs-on: ubuntu-24.04 - needs: [ci-test, ci-test-provider-aptos, ci-test-provider-canton, ci-test-provider-ton, ci-test-provider-tron, ci-test-provider-fast, ci-test-catalog-remote, ci-lint-misc, ci-lint] + needs: [ci-test, ci-test-provider-aptos, ci-test-provider-canton, ci-test-provider-ton, ci-test-provider-tron, ci-test-provider-stellar, ci-test-provider-fast, ci-test-catalog-remote, ci-lint-misc, ci-lint] permissions: contents: read actions: read diff --git a/chain/blockchain.go b/chain/blockchain.go index 12cad92d..822dde0a 100644 --- a/chain/blockchain.go +++ b/chain/blockchain.go @@ -13,6 +13,7 @@ import ( "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/stellar" "github.com/smartcontractkit/chainlink-deployments-framework/chain/sui" "github.com/smartcontractkit/chainlink-deployments-framework/chain/ton" "github.com/smartcontractkit/chainlink-deployments-framework/chain/tron" @@ -27,6 +28,7 @@ var _ BlockChain = sui.Chain{} var _ BlockChain = ton.Chain{} var _ BlockChain = tron.Chain{} var _ BlockChain = canton.Chain{} +var _ BlockChain = stellar.Chain{} // NetworkType represents the type of network, which can either be mainnet or testnet. type NetworkType string @@ -165,6 +167,11 @@ func (b BlockChains) CantonChains() map[uint64]canton.Chain { return getChainsByType[canton.Chain, *canton.Chain](b) } +// StellarChains returns a map of all Stellar chains with their selectors. +func (b BlockChains) StellarChains() map[uint64]stellar.Chain { + return getChainsByType[stellar.Chain, *stellar.Chain](b) +} + // ChainSelectorsOption defines a function type for configuring ChainSelectors type ChainSelectorsOption func(*chainSelectorsOptions) diff --git a/chain/blockchain_test.go b/chain/blockchain_test.go index 81481be9..742622ce 100644 --- a/chain/blockchain_test.go +++ b/chain/blockchain_test.go @@ -14,6 +14,7 @@ import ( "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/stellar" "github.com/smartcontractkit/chainlink-deployments-framework/chain/sui" "github.com/smartcontractkit/chainlink-deployments-framework/chain/ton" "github.com/smartcontractkit/chainlink-deployments-framework/chain/tron" @@ -27,6 +28,7 @@ var suiChain1 = sui.Chain{ChainMetadata: sui.ChainMetadata{Selector: chainsel.SU 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{ChainMetadata: canton.ChainMetadata{Selector: chainsel.CANTON_LOCALNET.Selector}} +var stellarChain1 = stellar.Chain{ChainMetadata: stellar.ChainMetadata{Selector: chainsel.STELLAR_LOCALNET.Selector}} func TestNewBlockChains(t *testing.T) { t.Parallel() @@ -165,6 +167,7 @@ func TestBlockChainsAllChains(t *testing.T) { solanaChain1.Selector, aptosChain1.Selector, suiChain1.Selector, tonChain1.Selector, tronChain1.Selector, cantonChain1.Selector, + stellarChain1.Selector, } assert.Len(t, allChains, len(expectedSelectors)) @@ -297,6 +300,21 @@ func TestBlockChainsGetters(t *testing.T) { } }, }, + { + name: "StellarChains", + runTest: func(t *testing.T, chains chain.BlockChains) { + t.Helper() + stellarChains := chains.StellarChains() + expectedSelectors := []uint64{stellarChain1.Selector} + assert.Len(t, stellarChains, len(expectedSelectors), "unexpected number of Stellar chains") + + for _, selector := range expectedSelectors { + _, exists := stellarChains[selector] + assert.True(t, exists, "expected Stellar chain with selector %d", selector) + } + }, + description: "expected Stellar chain selectors", + }, } // Run tests for both value and pointer chains @@ -337,6 +355,7 @@ func TestBlockChainsListChainSelectors(t *testing.T) { solanaChain1.ChainSelector(), aptosChain1.ChainSelector(), suiChain1.ChainSelector(), tonChain1.ChainSelector(), tronChain1.ChainSelector(), cantonChain1.ChainSelector(), + stellarChain1.ChainSelector(), }, description: "expected all chain selectors", }, @@ -388,12 +407,18 @@ func TestBlockChainsListChainSelectors(t *testing.T) { expectedIDs: []uint64{evmChain1.Selector, evmChain2.Selector, solanaChain1.Selector}, description: "expected EVM and Solana chain selectors", }, + { + name: "with family filter - Stellar", + options: []chain.ChainSelectorsOption{chain.WithFamily(chainsel.FamilyStellar)}, + expectedIDs: []uint64{stellarChain1.Selector}, + description: "expected Stellar chain selectors", + }, { name: "with exclusion", options: []chain.ChainSelectorsOption{chain.WithChainSelectorsExclusion( []uint64{evmChain1.Selector, aptosChain1.Selector}), }, - expectedIDs: []uint64{evmChain2.Selector, solanaChain1.Selector, suiChain1.Selector, tonChain1.Selector, tronChain1.Selector, cantonChain1.Selector}, + expectedIDs: []uint64{evmChain2.Selector, solanaChain1.Selector, suiChain1.Selector, tonChain1.Selector, tronChain1.Selector, cantonChain1.Selector, stellarChain1.Selector}, description: "expected chain selectors excluding 1 and 4", }, { @@ -418,17 +443,18 @@ func TestBlockChainsListChainSelectors(t *testing.T) { } // buildBlockChains creates a new BlockChains instance with the test chains. -// 2 evm chains, 1 solana chain, 1 aptos chain, 1 sui chain, 1 ton chain, 1 tron chain. +// 2 evm chains, 1 solana chain, 1 aptos chain, 1 sui chain, 1 ton chain, 1 tron chain, 1 canton chain, 1 stellar chain. func buildBlockChains() chain.BlockChains { chains := chain.NewBlockChains(map[uint64]chain.BlockChain{ - evmChain1.ChainSelector(): evmChain1, - solanaChain1.ChainSelector(): solanaChain1, - evmChain2.ChainSelector(): evmChain2, - aptosChain1.ChainSelector(): aptosChain1, - suiChain1.ChainSelector(): suiChain1, - tonChain1.ChainSelector(): tonChain1, - tronChain1.ChainSelector(): tronChain1, - cantonChain1.ChainSelector(): cantonChain1, + evmChain1.ChainSelector(): evmChain1, + solanaChain1.ChainSelector(): solanaChain1, + evmChain2.ChainSelector(): evmChain2, + aptosChain1.ChainSelector(): aptosChain1, + suiChain1.ChainSelector(): suiChain1, + tonChain1.ChainSelector(): tonChain1, + tronChain1.ChainSelector(): tronChain1, + cantonChain1.ChainSelector(): cantonChain1, + stellarChain1.ChainSelector(): stellarChain1, }) return chains @@ -454,6 +480,8 @@ func buildBlockChainsPointers() chain.BlockChains { pointerChains[selector] = &c case canton.Chain: pointerChains[selector] = &c + case stellar.Chain: + pointerChains[selector] = &c default: continue // skip unsupported chains } diff --git a/chain/mcms/adapters/chain_access.go b/chain/mcms/adapters/chain_access.go index 697fe6cf..b11f37ae 100644 --- a/chain/mcms/adapters/chain_access.go +++ b/chain/mcms/adapters/chain_access.go @@ -6,12 +6,14 @@ import ( solrpc "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/mcms/sdk/evm" mcmssui "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/stellar/go-stellar-sdk/clients/rpcclient" "github.com/xssnick/tonutils-go/ton" "github.com/smartcontractkit/chainlink-deployments-framework/chain" cldfaptos "github.com/smartcontractkit/chainlink-deployments-framework/chain/aptos" cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" cldfsol "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana" + cldfstellar "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" cldfsui "github.com/smartcontractkit/chainlink-deployments-framework/chain/sui" cldfton "github.com/smartcontractkit/chainlink-deployments-framework/chain/ton" ) @@ -23,6 +25,7 @@ type ChainsFetcher interface { AptosChains() map[uint64]cldfaptos.Chain SuiChains() map[uint64]cldfsui.Chain TonChains() map[uint64]cldfton.Chain + StellarChains() map[uint64]cldfstellar.Chain } // ChainAccessAdapter adapts CLDF's chain.BlockChains into a selector + lookup style API. @@ -90,3 +93,13 @@ func (a *ChainAccessAdapter) TonClient(selector uint64) (*ton.APIClient, bool) { return ch.Client, true } + +// StellarClient returns the Stellar RPC client for the given selector. +func (a *ChainAccessAdapter) StellarClient(selector uint64) (*rpcclient.Client, bool) { + ch, ok := a.inner.StellarChains()[selector] + if !ok { + return nil, false + } + + return ch.Client, true +} diff --git a/chain/mcms/adapters/chain_access_test.go b/chain/mcms/adapters/chain_access_test.go index 7dc10f64..02081613 100644 --- a/chain/mcms/adapters/chain_access_test.go +++ b/chain/mcms/adapters/chain_access_test.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/chain/aptos" "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" chainsui "github.com/smartcontractkit/chainlink-deployments-framework/chain/sui" "github.com/smartcontractkit/chainlink-deployments-framework/chain/ton" ) @@ -45,11 +46,12 @@ func TestChainAccess_SelectorsAndLookups(t *testing.T) { t.Parallel() const ( - evmSel = uint64(111) - solSel = uint64(222) - aptosSel = uint64(333) - suiSel = uint64(444) - tonSel = uint64(555) + evmSel = uint64(111) + solSel = uint64(222) + aptosSel = uint64(333) + suiSel = uint64(444) + tonSel = uint64(555) + stellarSel = uint64(666) ) evmOnchain := evm.NewMockOnchainClient(t) @@ -66,7 +68,8 @@ func TestChainAccess_SelectorsAndLookups(t *testing.T) { Client: nil, Signer: suiSigner, }, - tonSel: ton.Chain{ChainMetadata: ton.ChainMetadata{Selector: tonSel}, Client: nil}, + tonSel: ton.Chain{ChainMetadata: ton.ChainMetadata{Selector: tonSel}, Client: nil}, + stellarSel: stellar.Chain{ChainMetadata: stellar.ChainMetadata{Selector: stellarSel}, Client: nil}, }) a := Wrap(chains) @@ -92,4 +95,8 @@ func TestChainAccess_SelectorsAndLookups(t *testing.T) { gotTon, ok := a.TonClient(tonSel) require.True(t, ok) require.Nil(t, gotTon) + + gotStellar, ok := a.StellarClient(stellarSel) + require.True(t, ok) + require.Nil(t, gotStellar) } diff --git a/chain/stellar/provider/keypair_generator.go b/chain/stellar/provider/keypair_generator.go new file mode 100644 index 00000000..61118e58 --- /dev/null +++ b/chain/stellar/provider/keypair_generator.go @@ -0,0 +1,62 @@ +package provider + +import ( + "errors" + "fmt" + + "github.com/stellar/go-stellar-sdk/keypair" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" +) + +// KeypairGenerator is an interface for generating Stellar keypairs. +type KeypairGenerator interface { + Generate() (stellar.StellarSigner, error) +} + +// keypairFromHex is a KeypairGenerator that generates a keypair from a hex-encoded private key. +type keypairFromHex struct { + hexKey string +} + +var _ KeypairGenerator = (*keypairFromHex)(nil) + +// KeypairFromHex creates a KeypairGenerator that generates a keypair from a hex-encoded private key. +// The hex string can be with or without the "0x" prefix. +func KeypairFromHex(hexKey string) KeypairGenerator { + return &keypairFromHex{hexKey: hexKey} +} + +// Generate generates a Stellar keypair from the hex-encoded private key. +func (k *keypairFromHex) Generate() (stellar.StellarSigner, error) { + if k.hexKey == "" { + return nil, errors.New("hex key is empty") + } + + kp, err := stellar.KeypairFromHex(k.hexKey) + if err != nil { + return nil, fmt.Errorf("failed to create keypair from hex: %w", err) + } + + return stellar.NewStellarKeypairSigner(kp), nil +} + +// keypairRandom is a KeypairGenerator that generates a random keypair. +type keypairRandom struct{} + +var _ KeypairGenerator = (*keypairRandom)(nil) + +// KeypairRandom creates a KeypairGenerator that generates a random keypair. +func KeypairRandom() KeypairGenerator { + return &keypairRandom{} +} + +// Generate generates a random Stellar keypair. +func (k *keypairRandom) Generate() (stellar.StellarSigner, error) { + kp, err := keypair.Random() + if err != nil { + return nil, fmt.Errorf("failed to generate random keypair: %w", err) + } + + return stellar.NewStellarKeypairSigner(kp), nil +} diff --git a/chain/stellar/provider/keypair_generator_test.go b/chain/stellar/provider/keypair_generator_test.go new file mode 100644 index 00000000..3959552a --- /dev/null +++ b/chain/stellar/provider/keypair_generator_test.go @@ -0,0 +1,191 @@ +package provider + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKeypairFromHex_Generate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + hexKey string + wantErr string + }{ + { + name: "valid hex key without prefix", + hexKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + { + name: "valid hex key with 0x prefix", + hexKey: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + { + name: "empty hex key", + hexKey: "", + wantErr: "hex key is empty", + }, + { + name: "invalid hex", + hexKey: "not_valid_hex", + wantErr: "failed to create keypair from hex", + }, + { + name: "wrong length", + hexKey: "0123456789abcdef", + wantErr: "failed to create keypair from hex", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + gen := KeypairFromHex(tt.hexKey) + require.NotNil(t, gen) + + signer, err := gen.Generate() + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + assert.Nil(t, signer) + + return + } + + require.NoError(t, err) + require.NotNil(t, signer) + + // Verify the signer works + address := signer.Address() + assert.NotEmpty(t, address) + + message := []byte("test message") + sig, err := signer.Sign(message) + require.NoError(t, err) + assert.NotNil(t, sig) + }) + } +} + +func TestKeypairFromHex_GenerateConsistent(t *testing.T) { + t.Parallel() + + hexKey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + gen := KeypairFromHex(hexKey) + + // Generate twice + signer1, err := gen.Generate() + require.NoError(t, err) + + signer2, err := gen.Generate() + require.NoError(t, err) + + // Both should produce the same address + assert.Equal(t, signer1.Address(), signer2.Address()) +} + +func TestKeypairRandom_Generate(t *testing.T) { + t.Parallel() + + gen := KeypairRandom() + require.NotNil(t, gen) + + signer, err := gen.Generate() + require.NoError(t, err) + require.NotNil(t, signer) + + // Verify the signer works + address := signer.Address() + assert.NotEmpty(t, address) + + message := []byte("test message") + sig, err := signer.Sign(message) + require.NoError(t, err) + assert.NotNil(t, sig) +} + +func TestKeypairRandom_GenerateUnique(t *testing.T) { + t.Parallel() + + gen := KeypairRandom() + + // Generate multiple times + addresses := make(map[string]bool) + for range 10 { + signer, err := gen.Generate() + require.NoError(t, err) + + address := signer.Address() + assert.NotEmpty(t, address) + + // Each address should be unique (since they're random) + assert.False(t, addresses[address], "address %s was generated more than once", address) + addresses[address] = true + } + + assert.Len(t, addresses, 10, "should have generated 10 unique addresses") +} + +func TestKeypairFromHex_GenerateWithDifferentKeys(t *testing.T) { + t.Parallel() + + hexKey1 := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + hexKey2 := "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" + + gen1 := KeypairFromHex(hexKey1) + gen2 := KeypairFromHex(hexKey2) + + signer1, err := gen1.Generate() + require.NoError(t, err) + + signer2, err := gen2.Generate() + require.NoError(t, err) + + // Different keys should produce different addresses + assert.NotEqual(t, signer1.Address(), signer2.Address()) +} + +func TestKeypairFromHex_GenerateSignAndVerify(t *testing.T) { + t.Parallel() + + hexKey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + gen := KeypairFromHex(hexKey) + signer, err := gen.Generate() + require.NoError(t, err) + + message := []byte("test message for signing") + sig, err := signer.Sign(message) + require.NoError(t, err) + assert.NotNil(t, sig) + assert.NotEmpty(t, sig) + + // Generate another signer with the same key to verify cross-compatibility + gen2 := KeypairFromHex(hexKey) + signer2, err := gen2.Generate() + require.NoError(t, err) + + assert.Equal(t, signer.Address(), signer2.Address(), "same key should produce same address") +} + +func TestKeypairRandom_GenerateMultipleFromSameGen(t *testing.T) { + t.Parallel() + + gen := KeypairRandom() + + // Generate from the same generator multiple times + // Each call should produce a different random keypair + signer1, err := gen.Generate() + require.NoError(t, err) + + signer2, err := gen.Generate() + require.NoError(t, err) + + // Should be different since they're random + assert.NotEqual(t, signer1.Address(), signer2.Address()) +} diff --git a/chain/stellar/provider/rpc_provider.go b/chain/stellar/provider/rpc_provider.go new file mode 100644 index 00000000..96bc37fa --- /dev/null +++ b/chain/stellar/provider/rpc_provider.go @@ -0,0 +1,104 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/stellar/go-stellar-sdk/clients/rpcclient" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" +) + +type RPCChainProviderConfig struct { + // Required: The Soroban RPC URL to connect to the Stellar network + SorobanRPCURL string + + // Required: The network passphrase identifying the Stellar network + NetworkPassphrase string + + // Optional: The Friendbot URL for funding test accounts (only required for testing environments) + FriendbotURL string + + // Required: A generator for the deployer keypair. Use KeypairFromHex to create a deployer + // keypair from a hex-encoded private key. + DeployerKeypairGen KeypairGenerator +} + +func (c RPCChainProviderConfig) validate() error { + if c.SorobanRPCURL == "" { + return errors.New("soroban RPC URL is required") + } + if c.NetworkPassphrase == "" { + return errors.New("network passphrase is required") + } + // Note: FriendbotURL is optional; it's only required for testing environments + if c.DeployerKeypairGen == nil { + return errors.New("deployer keypair generator is required") + } + + return nil +} + +type RPCChainProvider struct { + selector uint64 + config RPCChainProviderConfig + + chain *stellar.Chain +} + +var _ chain.Provider = (*RPCChainProvider)(nil) + +func NewRPCChainProvider(selector uint64, config RPCChainProviderConfig) *RPCChainProvider { + return &RPCChainProvider{ + selector: selector, + config: config, + } +} + +func (p *RPCChainProvider) Initialize(_ context.Context) (chain.BlockChain, error) { + if p.chain != nil { + return p.chain, nil // already initialized + } + + if err := p.config.validate(); err != nil { + return nil, fmt.Errorf("failed to validate provider config: %w", err) + } + + // Generate the deployer keypair + deployerSigner, err := p.config.DeployerKeypairGen.Generate() + if err != nil { + return nil, fmt.Errorf("failed to generate deployer keypair: %w", err) + } + + // Create the Soroban RPC client + client := rpcclient.NewClient(p.config.SorobanRPCURL, &http.Client{ + Timeout: 60 * time.Second, + }) + + p.chain = &stellar.Chain{ + ChainMetadata: stellar.ChainMetadata{Selector: p.selector}, + Client: client, + Signer: deployerSigner, + URL: p.config.SorobanRPCURL, + FriendbotURL: p.config.FriendbotURL, + NetworkPassphrase: p.config.NetworkPassphrase, + } + + return p.chain, nil +} + +func (p *RPCChainProvider) Name() string { + return "Stellar RPC Chain Provider" +} + +func (p *RPCChainProvider) ChainSelector() uint64 { + return p.selector +} + +func (p *RPCChainProvider) BlockChain() chain.BlockChain { + return p.chain +} diff --git a/chain/stellar/provider/rpc_provider_test.go b/chain/stellar/provider/rpc_provider_test.go new file mode 100644 index 00000000..c13cfc73 --- /dev/null +++ b/chain/stellar/provider/rpc_provider_test.go @@ -0,0 +1,217 @@ +package provider + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" +) + +func Test_RPCChainProvider_Initialize(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + giveSelector uint64 + giveConfig RPCChainProviderConfig + giveChain *stellar.Chain // pre-existing chain for re-initialization test + wantErr string + }{ + { + name: "valid initialization", + giveSelector: 12345, + giveConfig: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairFromHex("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"), + }, + }, + { + name: "re-initialize returns existing chain", + giveSelector: 67890, + giveConfig: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), + }, + giveChain: &stellar.Chain{ + ChainMetadata: stellar.ChainMetadata{Selector: 67890}, + }, + }, + { + name: "fails config validation - missing network passphrase", + giveSelector: 12345, + giveConfig: RPCChainProviderConfig{ + NetworkPassphrase: "", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), + }, + wantErr: "network passphrase is required", + }, + { + name: "missing friendbot URL - allowed since optional", + giveSelector: 12345, + giveConfig: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), + }, + wantErr: "", // FriendbotURL is optional + }, + { + name: "fails config validation - missing soroban RPC URL", + giveSelector: 12345, + giveConfig: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "", + DeployerKeypairGen: KeypairRandom(), + }, + wantErr: "soroban RPC URL is required", + }, + { + name: "fails config validation - missing deployer keypair generator", + giveSelector: 12345, + giveConfig: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: nil, + }, + wantErr: "deployer keypair generator is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + p := NewRPCChainProvider(tt.giveSelector, tt.giveConfig) + if tt.giveChain != nil { + p.chain = tt.giveChain + } + + got, err := p.Initialize(context.Background()) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.NotNil(t, p.chain) + + gotChain, ok := got.(*stellar.Chain) + require.True(t, ok, "expected got to be of type *stellar.Chain") + assert.Equal(t, tt.giveSelector, gotChain.Selector) + + // If we had a pre-existing chain, verify it's the same instance + // The re-initialization returns early without re-populating fields + if tt.giveChain != nil { + assert.Equal(t, tt.giveChain, gotChain) + } else { + // For fresh initialization, verify all fields are populated + assert.NotNil(t, gotChain.Client, "RPC client should be initialized") + assert.NotNil(t, gotChain.Signer, "Signer should be initialized") + assert.Equal(t, tt.giveConfig.SorobanRPCURL, gotChain.URL) + assert.Equal(t, tt.giveConfig.FriendbotURL, gotChain.FriendbotURL) + assert.Equal(t, tt.giveConfig.NetworkPassphrase, gotChain.NetworkPassphrase) + } + }) + } +} + +func Test_RPCChainProvider_Name(t *testing.T) { + t.Parallel() + + p := &RPCChainProvider{} + assert.Equal(t, "Stellar RPC Chain Provider", p.Name()) +} + +func Test_RPCChainProvider_ChainSelector(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + giveSelector uint64 + }{ + { + name: "selector 12345", + giveSelector: 12345, + }, + { + name: "selector 0", + giveSelector: 0, + }, + { + name: "large selector", + giveSelector: 999999999999, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + p := &RPCChainProvider{selector: tt.giveSelector} + assert.Equal(t, tt.giveSelector, p.ChainSelector()) + }) + } +} + +func Test_RPCChainProvider_BlockChain(t *testing.T) { + t.Parallel() + + t.Run("returns nil when chain is nil", func(t *testing.T) { + t.Parallel() + + p := &RPCChainProvider{ + chain: nil, + } + + got := p.BlockChain() + assert.Nil(t, got) + }) + + t.Run("returns chain when initialized", func(t *testing.T) { + t.Parallel() + + chain := &stellar.Chain{ + ChainMetadata: stellar.ChainMetadata{Selector: 12345}, + } + + p := &RPCChainProvider{ + chain: chain, + } + + got := p.BlockChain() + assert.Equal(t, chain, got) + }) +} + +func Test_NewRPCChainProvider(t *testing.T) { + t.Parallel() + + selector := uint64(12345) + config := RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), + } + + p := NewRPCChainProvider(selector, config) + + require.NotNil(t, p) + assert.Equal(t, selector, p.selector) + assert.Equal(t, config.NetworkPassphrase, p.config.NetworkPassphrase) + assert.Equal(t, config.FriendbotURL, p.config.FriendbotURL) + assert.Equal(t, config.SorobanRPCURL, p.config.SorobanRPCURL) + assert.Nil(t, p.chain, "chain should not be initialized until Initialize() is called") +} diff --git a/chain/stellar/signer.go b/chain/stellar/signer.go new file mode 100644 index 00000000..e31c6edb --- /dev/null +++ b/chain/stellar/signer.go @@ -0,0 +1,77 @@ +package stellar + +import ( + "encoding/hex" + "fmt" + "strings" + + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stellar/go-stellar-sdk/xdr" +) + +// StellarSigner is an interface that provides signing capabilities for Stellar transactions. +type StellarSigner interface { + // Sign signs the given message and returns the signature bytes. + Sign(message []byte) ([]byte, error) + + // SignDecorated signs the given message and returns a decorated signature (XDR format). + SignDecorated(message []byte) (xdr.DecoratedSignature, error) + + // Address returns the Stellar address derived from the signer's public key. + Address() string +} + +// stellarKeypairSigner implements StellarSigner using a keypair.Full from the Stellar SDK. +type stellarKeypairSigner struct { + kp *keypair.Full +} + +var _ StellarSigner = (*stellarKeypairSigner)(nil) + +// NewStellarKeypairSigner creates a new StellarSigner from a keypair.Full. +func NewStellarKeypairSigner(kp *keypair.Full) StellarSigner { + return &stellarKeypairSigner{kp: kp} +} + +// Sign signs the given message and returns the signature bytes. +func (s *stellarKeypairSigner) Sign(message []byte) ([]byte, error) { + return s.kp.Sign(message) +} + +// SignDecorated signs the given message and returns a decorated signature. +func (s *stellarKeypairSigner) SignDecorated(message []byte) (xdr.DecoratedSignature, error) { + return s.kp.SignDecorated(message) +} + +// Address returns the Stellar address. +func (s *stellarKeypairSigner) Address() string { + return s.kp.Address() +} + +// KeypairFromHex creates a keypair.Full from a hex-encoded private key. +// The hex string can be with or without the "0x" prefix. +func KeypairFromHex(hexKey string) (*keypair.Full, error) { + // Remove "0x" prefix if present + hexKey = strings.TrimPrefix(hexKey, "0x") + + // Decode hex to bytes + rawSeed, err := hex.DecodeString(hexKey) + if err != nil { + return nil, fmt.Errorf("failed to decode hex key: %w", err) + } + + // Stellar keypairs use 32-byte seeds + if len(rawSeed) != 32 { + return nil, fmt.Errorf("invalid key length: expected 32 bytes, got %d", len(rawSeed)) + } + + var seed [32]byte + copy(seed[:], rawSeed) + + kp, err := keypair.FromRawSeed(seed) + if err != nil { + return nil, fmt.Errorf("failed to create keypair from seed: %w", err) + } + + return kp, nil +} diff --git a/chain/stellar/signer_test.go b/chain/stellar/signer_test.go new file mode 100644 index 00000000..de547153 --- /dev/null +++ b/chain/stellar/signer_test.go @@ -0,0 +1,323 @@ +package stellar + +import ( + "encoding/hex" + "testing" + + "github.com/stellar/go-stellar-sdk/keypair" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewStellarKeypairSigner(t *testing.T) { + t.Parallel() + + kp, err := keypair.Random() + require.NoError(t, err) + + signer := NewStellarKeypairSigner(kp) + require.NotNil(t, signer) + + assert.Equal(t, kp.Address(), signer.Address()) +} + +func TestStellarKeypairSigner_Sign(t *testing.T) { + t.Parallel() + + kp, err := keypair.Random() + require.NoError(t, err) + + signer := NewStellarKeypairSigner(kp) + + message := []byte("test message") + sig, err := signer.Sign(message) + require.NoError(t, err) + assert.NotNil(t, sig) + + // Verify the signature using the keypair + err = kp.Verify(message, sig) + assert.NoError(t, err, "signature should be valid") +} + +func TestStellarKeypairSigner_SignDecorated(t *testing.T) { + t.Parallel() + + kp, err := keypair.Random() + require.NoError(t, err) + + signer := NewStellarKeypairSigner(kp) + + message := []byte("test message") + decoratedSig, err := signer.SignDecorated(message) + require.NoError(t, err) + + // XDR DecoratedSignature should have hint and signature + // Note: decoratedSig.Hint is xdr.SignatureHint which wraps [4]byte + expectedHint := kp.Hint() + assert.Equal(t, expectedHint[:], decoratedSig.Hint[:]) + assert.NotEmpty(t, decoratedSig.Signature) +} + +func TestStellarKeypairSigner_Address(t *testing.T) { + t.Parallel() + + kp, err := keypair.Random() + require.NoError(t, err) + + signer := NewStellarKeypairSigner(kp) + address := signer.Address() + + assert.NotEmpty(t, address) + assert.Equal(t, kp.Address(), address) +} + +func TestKeypairFromHex(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + hexKey string + wantErr string + }{ + { + name: "valid 32-byte hex key without prefix", + hexKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + { + name: "valid 32-byte hex key with 0x prefix", + hexKey: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + { + name: "invalid hex string", + hexKey: "not_valid_hex", + wantErr: "failed to decode hex key", + }, + { + name: "wrong length - too short", + hexKey: "0123456789abcdef", + wantErr: "invalid key length: expected 32 bytes, got 8", + }, + { + name: "wrong length - too long", + hexKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + wantErr: "invalid key length: expected 32 bytes, got 40", + }, + { + name: "empty string", + hexKey: "", + wantErr: "invalid key length: expected 32 bytes, got 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + kp, err := KeypairFromHex(tt.hexKey) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + assert.Nil(t, kp) + + return + } + + require.NoError(t, err) + require.NotNil(t, kp) + + // Verify the keypair can sign + message := []byte("test") + sig, err := kp.Sign(message) + require.NoError(t, err) + assert.NotNil(t, sig) + + // Verify the signature + err = kp.Verify(message, sig) + require.NoError(t, err) + }) + } +} + +func TestKeypairFromHex_ConsistentAddress(t *testing.T) { + t.Parallel() + + // Use a known seed to test consistency + hexKey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + // Parse twice + kp1, err := KeypairFromHex(hexKey) + require.NoError(t, err) + + kp2, err := KeypairFromHex(hexKey) + require.NoError(t, err) + + // Both should produce the same address + assert.Equal(t, kp1.Address(), kp2.Address()) + + // Verify the keypairs can sign and verify each other's signatures + message := []byte("test message") + + sig1, err := kp1.Sign(message) + require.NoError(t, err) + + sig2, err := kp2.Sign(message) + require.NoError(t, err) + + // Signatures should be identical for the same message and keypair + assert.Equal(t, sig1, sig2) + + // Each keypair should be able to verify the other's signature + err = kp1.Verify(message, sig2) + require.NoError(t, err) + + err = kp2.Verify(message, sig1) + require.NoError(t, err) +} + +func TestKeypairFromHex_RealStellarKey(t *testing.T) { + t.Parallel() + + // We can't directly get the raw seed from SDK's Full keypair, + // so we'll just verify that our KeypairFromHex works with a properly formatted hex + testHex := "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" + reconstructedKp, err := KeypairFromHex(testHex) + require.NoError(t, err) + + // Verify it can sign + message := []byte("test") + sig, err := reconstructedKp.Sign(message) + require.NoError(t, err) + + err = reconstructedKp.Verify(message, sig) + require.NoError(t, err) + + // Verify the address is a valid Stellar address format (starts with G) + address := reconstructedKp.Address() + assert.NotEmpty(t, address) + assert.Equal(t, 'G', rune(address[0]), "Stellar public addresses start with 'G'") +} + +func TestKeypairFromHex_WithAndWithoutPrefix(t *testing.T) { + t.Parallel() + + hexWithout := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + hexWith := "0x" + hexWithout + + kp1, err := KeypairFromHex(hexWithout) + require.NoError(t, err) + + kp2, err := KeypairFromHex(hexWith) + require.NoError(t, err) + + // Both should produce the same address + assert.Equal(t, kp1.Address(), kp2.Address()) +} + +func TestStellarSigner_Interface(t *testing.T) { + t.Parallel() + + // Verify stellarKeypairSigner implements StellarSigner + var _ StellarSigner = (*stellarKeypairSigner)(nil) + + kp, err := keypair.Random() + require.NoError(t, err) + + signer := NewStellarKeypairSigner(kp) + require.NotNil(t, signer) + + // Test all interface methods + address := signer.Address() + assert.NotEmpty(t, address) + + message := []byte("test") + sig, err := signer.Sign(message) + require.NoError(t, err) + assert.NotNil(t, sig) + + decoratedSig, err := signer.SignDecorated(message) + require.NoError(t, err) + assert.NotEmpty(t, decoratedSig.Signature) +} + +func TestKeypairFromHex_EdgeCases(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + hexKey string + wantErr string + }{ + { + name: "all zeros", + hexKey: "0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "all ones", + hexKey: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + { + name: "uppercase hex", + hexKey: "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789", + }, + { + name: "mixed case hex", + hexKey: "AbCdEf0123456789aBcDeF0123456789AbCdEf0123456789aBcDeF0123456789", + }, + { + name: "odd-length hex after 0x removal", + hexKey: "0x123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde", + wantErr: "invalid key length", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + kp, err := KeypairFromHex(tt.hexKey) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + assert.Nil(t, kp) + + return + } + + require.NoError(t, err) + require.NotNil(t, kp) + + // Verify it can be used + message := []byte("test") + sig, err := kp.Sign(message) + require.NoError(t, err) + + err = kp.Verify(message, sig) + assert.NoError(t, err) + }) + } +} + +func TestKeypairFromHex_ByteConversion(t *testing.T) { + t.Parallel() + + // Test with known bytes + expectedBytes := make([]byte, 32) + for i := range expectedBytes { + expectedBytes[i] = byte(i) + } + + hexKey := hex.EncodeToString(expectedBytes) + require.Len(t, hexKey, 64, "hex encoding of 32 bytes should be 64 chars") + + kp, err := KeypairFromHex(hexKey) + require.NoError(t, err) + require.NotNil(t, kp) + + // Verify the keypair works + message := []byte("test") + sig, err := kp.Sign(message) + require.NoError(t, err) + + err = kp.Verify(message, sig) + assert.NoError(t, err) +} diff --git a/chain/stellar/stellar_chain.go b/chain/stellar/stellar_chain.go new file mode 100644 index 00000000..a35c4d3b --- /dev/null +++ b/chain/stellar/stellar_chain.go @@ -0,0 +1,29 @@ +package stellar + +import ( + "github.com/stellar/go-stellar-sdk/clients/rpcclient" + + chaincommon "github.com/smartcontractkit/chainlink-deployments-framework/chain/internal/common" +) + +type ChainMetadata = chaincommon.ChainMetadata + +// Chain represents a Stellar network instance used by the Chainlink Deployments Framework (CLDF). +type Chain struct { + ChainMetadata + + // Client is the Soroban RPC client for interacting with the Stellar network + Client *rpcclient.Client + + // Signer is the keypair used for signing transactions + Signer StellarSigner + + // URL is the Soroban RPC endpoint URL + URL string + + // FriendbotURL is the Friendbot endpoint URL for funding test accounts (optional, only required for testing) + FriendbotURL string + + // NetworkPassphrase identifies the Stellar network + NetworkPassphrase string +} diff --git a/engine/cld/chains/chains.go b/engine/cld/chains/chains.go index 69c3fa4a..9b3259fb 100644 --- a/engine/cld/chains/chains.go +++ b/engine/cld/chains/chains.go @@ -18,6 +18,7 @@ import ( evmprov "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/provider" evmclient "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/provider/rpcclient" solanaprov "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana/provider" + stellarprov "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar/provider" suiprov "github.com/smartcontractkit/chainlink-deployments-framework/chain/sui/provider" tonprov "github.com/smartcontractkit/chainlink-deployments-framework/chain/ton/provider" tronprov "github.com/smartcontractkit/chainlink-deployments-framework/chain/tron/provider" @@ -202,6 +203,12 @@ func newChainLoaders( lggr.Info("Skipping Sui chains, no private key found in secrets") } + if cfg.Stellar.DeployerKey != "" { + loaders[chainsel.FamilyStellar] = newChainLoaderStellar(networks, cfg) + } else { + lggr.Info("Skipping Stellar chains, no private key found in secrets") + } + if cfg.Ton.DeployerKey != "" { loaders[chainsel.FamilyTon] = newChainLoaderTon(networks, cfg) } else { @@ -217,6 +224,7 @@ var ( _ ChainLoader = &chainLoaderEVM{} _ ChainLoader = &chainLoaderTron{} _ ChainLoader = &chainLoaderSui{} + _ ChainLoader = &chainLoaderStellar{} ) // ChainLoader is an interface that defines the methods for loading a chain. @@ -324,6 +332,53 @@ func (l *chainLoaderSui) Load(ctx context.Context, selector uint64) (fchain.Bloc return c, nil } +// chainLoaderStellar implements the ChainLoader interface for Stellar. +type chainLoaderStellar struct { + *baseChainLoader +} + +// newChainLoaderStellar creates a new chain loader for Stellar. +func newChainLoaderStellar( + networks *cfgnet.Config, cfg cfgenv.OnchainConfig, +) *chainLoaderStellar { + return &chainLoaderStellar{ + baseChainLoader: newBaseChainLoader(networks, cfg), + } +} + +// Load loads a Stellar Chain for a selector. +// RPC URL (Soroban) comes from network.RPCs like other chains; passphrase and Friendbot URL from metadata. +func (l *chainLoaderStellar) Load(ctx context.Context, selector uint64) (fchain.BlockChain, error) { + network, err := l.getNetwork(selector) + if err != nil { + return nil, err + } + + rpcURL := network.RPCs[0].HTTPURL + if rpcURL == "" { + return nil, fmt.Errorf("stellar network %d: RPC http_url is required", selector) + } + + md, err := cfgnet.DecodeMetadata[cfgnet.StellarMetadata](network.Metadata) + if err != nil { + return nil, fmt.Errorf("stellar network %d: decode metadata: %w", selector, err) + } + + c, err := stellarprov.NewRPCChainProvider(selector, + stellarprov.RPCChainProviderConfig{ + NetworkPassphrase: md.NetworkPassphrase, + FriendbotURL: md.FriendbotURL, + SorobanRPCURL: rpcURL, + DeployerKeypairGen: stellarprov.KeypairFromHex(l.cfg.Stellar.DeployerKey), + }, + ).Initialize(ctx) + if err != nil { + return nil, fmt.Errorf("failed to initialize Stellar chain %d: %w", selector, err) + } + + return c, nil +} + // chainLoaderSolana implements the ChainLoader interface for Solana. type chainLoaderSolana struct { *baseChainLoader diff --git a/engine/cld/chains/chains_test.go b/engine/cld/chains/chains_test.go index 250ebb4d..fa8c032b 100644 --- a/engine/cld/chains/chains_test.go +++ b/engine/cld/chains/chains_test.go @@ -54,6 +54,9 @@ func Test_newChainLoaders(t *testing.T) { Ton: cfgenv.TonConfig{ DeployerKey: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", }, + Stellar: cfgenv.StellarConfig{ + DeployerKey: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, }, wantLoaders: []string{ chainsel.FamilyEVM, @@ -62,6 +65,7 @@ func Test_newChainLoaders(t *testing.T) { chainsel.FamilyAptos, chainsel.FamilySui, chainsel.FamilyTon, + chainsel.FamilyStellar, }, }, { @@ -135,11 +139,12 @@ func Test_LoadChains(t *testing.T) { var ( fakeSrv = newFakeRPCServer(t) - evmSelector = chainsel.TEST_1000.Selector - solanaSelector = chainsel.TEST_22222222222222222222222222222222222222222222.Selector - aptosSelector = chainsel.APTOS_LOCALNET.Selector - tronSelector = chainsel.TRON_TESTNET_NILE.Selector - suiSelector = chainsel.SUI_LOCALNET.Selector + evmSelector = chainsel.TEST_1000.Selector + solanaSelector = chainsel.TEST_22222222222222222222222222222222222222222222.Selector + aptosSelector = chainsel.APTOS_LOCALNET.Selector + tronSelector = chainsel.TRON_TESTNET_NILE.Selector + suiSelector = chainsel.SUI_LOCALNET.Selector + stellarSelector = chainsel.STELLAR_TESTNET.Selector // tonSelector = chainsel.TON_TESTNET.Selector ) @@ -204,6 +209,22 @@ func Test_LoadChains(t *testing.T) { }, }, }, + { + Type: cfgnet.NetworkTypeTestnet, + ChainSelector: stellarSelector, + RPCs: []cfgnet.RPC{ + { + RPCName: "stellar_rpc", + PreferredURLScheme: "http", + HTTPURL: "http://stellar-rpc", + WSURL: "", + }, + }, + Metadata: cfgnet.StellarMetadata{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + }, + }, // TON API Client requires to connect to a liteserver on initialization, // however, testing with actual liteserver could be fragile, disabling test for now: // { @@ -257,6 +278,9 @@ func Test_LoadChains(t *testing.T) { DeployerKey: "0b1f7dbb19112fdac53344cf49731e41bfc420ac6a71d38c89fb38d04a6563d99aa3d1fa430550e8de5171ec55453b4e048c1701cadfa56726d489c56d67bab3", // Mock private key WalletVersion: "V4R2", }, + Stellar: cfgenv.StellarConfig{ + DeployerKey: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, } tests := []struct { @@ -271,8 +295,8 @@ func Test_LoadChains(t *testing.T) { name: "loads all valid chains", giveNetworkConfig: networksConfig, giveOnchainConfig: onchainConfig, - giveSelectors: []uint64{evmSelector, solanaSelector, aptosSelector, tronSelector, suiSelector}, - wantCount: 5, + giveSelectors: []uint64{evmSelector, solanaSelector, aptosSelector, tronSelector, suiSelector, stellarSelector}, + wantCount: 6, }, { name: "fails with unknown selector", @@ -568,6 +592,206 @@ func Test_chainLoaderSui_Load(t *testing.T) { } } +func Test_chainLoaderStellar_Load(t *testing.T) { + t.Parallel() + + stellarSelector := chainsel.STELLAR_TESTNET.Selector + + networkCfg := cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeTestnet, + ChainSelector: stellarSelector, + RPCs: []cfgnet.RPC{ + { + RPCName: "stellar_testnet_rpc", + PreferredURLScheme: "http", + HTTPURL: "https://soroban-testnet.stellar.org", + WSURL: "", // Not used for Stellar + }, + }, + Metadata: cfgnet.StellarMetadata{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + }, + }, + { + Type: cfgnet.NetworkTypeTestnet, + ChainSelector: 999999, // Different chain for testing + RPCs: []cfgnet.RPC{ + { + RPCName: "other_chain_rpc", + PreferredURLScheme: "http", + HTTPURL: "https://other.rpc.com", + WSURL: "", + }, + }, + }, + }) + + onchainConfig := cfgenv.OnchainConfig{ + Stellar: cfgenv.StellarConfig{ + DeployerKey: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + } + + tests := []struct { + name string + giveSelector uint64 + giveNetworkConfig *cfgnet.Config + giveOnchainConfig cfgenv.OnchainConfig + wantErr string + }{ + { + name: "successful load", + giveSelector: stellarSelector, + giveNetworkConfig: networkCfg, + giveOnchainConfig: onchainConfig, + }, + { + name: "network not found", + giveSelector: 88888888, // Non-existent chain selector + giveNetworkConfig: networkCfg, + giveOnchainConfig: onchainConfig, + wantErr: "not found in configuration", + }, + { + name: "no RPCs configured", + giveSelector: 777777, + giveNetworkConfig: cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeTestnet, + ChainSelector: 777777, + RPCs: []cfgnet.RPC{}, + }, + }), + giveOnchainConfig: onchainConfig, + wantErr: "no RPCs found for chain selector: 777777", + }, + { + name: "missing RPC HTTP URL", + giveSelector: 666666, + giveNetworkConfig: cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeTestnet, + ChainSelector: 666666, + RPCs: []cfgnet.RPC{ + { + RPCName: "stellar_rpc", + PreferredURLScheme: "http", + HTTPURL: "", // Empty HTTP URL + WSURL: "", + }, + }, + Metadata: cfgnet.StellarMetadata{ + NetworkPassphrase: "Test Network", + FriendbotURL: "https://friendbot.stellar.org", + }, + }, + }), + giveOnchainConfig: onchainConfig, + wantErr: "RPC http_url is required", + }, + { + name: "missing metadata", + giveSelector: 555555, + giveNetworkConfig: cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeTestnet, + ChainSelector: 555555, + RPCs: []cfgnet.RPC{ + { + RPCName: "stellar_rpc", + PreferredURLScheme: "http", + HTTPURL: "https://soroban-testnet.stellar.org", + WSURL: "", + }, + }, + // No Metadata field at all + }, + }), + giveOnchainConfig: onchainConfig, + wantErr: "decode metadata", + }, + { + name: "missing network passphrase in metadata", + giveSelector: 444444, + giveNetworkConfig: cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeTestnet, + ChainSelector: 444444, + RPCs: []cfgnet.RPC{ + { + RPCName: "stellar_rpc", + PreferredURLScheme: "http", + HTTPURL: "https://soroban-testnet.stellar.org", + WSURL: "", + }, + }, + Metadata: cfgnet.StellarMetadata{ + NetworkPassphrase: "", // Empty passphrase + FriendbotURL: "https://friendbot.stellar.org", + }, + }, + }), + giveOnchainConfig: onchainConfig, + wantErr: "network passphrase is required", + }, + { + name: "missing friendbot URL in metadata - allowed since optional", + giveSelector: 333333, + giveNetworkConfig: cfgnet.NewConfig([]cfgnet.Network{ + { + Type: cfgnet.NetworkTypeTestnet, + ChainSelector: 333333, + RPCs: []cfgnet.RPC{ + { + RPCName: "stellar_rpc", + PreferredURLScheme: "http", + HTTPURL: "https://soroban-testnet.stellar.org", + WSURL: "", + }, + }, + Metadata: cfgnet.StellarMetadata{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "", // Empty friendbot URL (optional) + }, + }, + }), + giveOnchainConfig: onchainConfig, + wantErr: "", // FriendbotURL is optional + }, + // Note: Empty deployer key test is omitted because Stellar RPC provider + // doesn't validate the deployer key at initialization time. + // It would only fail when trying to use the key for actual operations. + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + loader := newChainLoaderStellar(tt.giveNetworkConfig, tt.giveOnchainConfig) + require.NotNil(t, loader) + + ctx := t.Context() + + chain, err := loader.Load(ctx, tt.giveSelector) + + if tt.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErr) + assert.Nil(t, chain) + } else { + require.NoError(t, err) + require.NotNil(t, chain) + + assert.Equal(t, tt.giveSelector, chain.ChainSelector()) + // Note: Family() returns empty string for test selectors not in chain-selectors + // Once STELLAR_LOCALNET is fully supported, we can use it and assert the family + } + }) + } +} + func Test_ChainLoaderSolana_Load(t *testing.T) { t.Parallel() diff --git a/engine/cld/config/env/config.go b/engine/cld/config/env/config.go index 71d2cd58..0df09b09 100644 --- a/engine/cld/config/env/config.go +++ b/engine/cld/config/env/config.go @@ -58,6 +58,14 @@ type AptosConfig struct { DeployerKey string `mapstructure:"deployer_key" yaml:"deployer_key"` // Secret: The private key of the deployer account. } +// StellarConfig is the configuration for the Stellar Chains. +// +// WARNING: This data type contains sensitive fields and should not be logged or set in file +// configuration. +type StellarConfig struct { + DeployerKey string `mapstructure:"deployer_key" yaml:"deployer_key"` // Secret: The private key of the deployer account. +} + // SuiConfig is the configuration for the Sui Chains. // // WARNING: This data type contains sensitive fields and should not be logged or set in file @@ -124,13 +132,14 @@ type CatalogConfig struct { // OnchainConfig wraps the configuration for the onchain components. type OnchainConfig struct { - KMS KMSConfig `mapstructure:"kms" yaml:"kms"` - EVM EVMConfig `mapstructure:"evm" yaml:"evm"` - Solana SolanaConfig `mapstructure:"solana" yaml:"solana"` - Aptos AptosConfig `mapstructure:"aptos" yaml:"aptos"` - Sui SuiConfig `mapstructure:"sui" yaml:"sui"` - Tron TronConfig `mapstructure:"tron" yaml:"tron"` - Ton TonConfig `mapstructure:"ton" yaml:"ton"` + KMS KMSConfig `mapstructure:"kms" yaml:"kms"` + EVM EVMConfig `mapstructure:"evm" yaml:"evm"` + Solana SolanaConfig `mapstructure:"solana" yaml:"solana"` + Aptos AptosConfig `mapstructure:"aptos" yaml:"aptos"` + Sui SuiConfig `mapstructure:"sui" yaml:"sui"` + Stellar StellarConfig `mapstructure:"stellar" yaml:"stellar"` + Tron TronConfig `mapstructure:"tron" yaml:"tron"` + Ton TonConfig `mapstructure:"ton" yaml:"ton"` } // OffchainConfig wraps the configuration for the offchain components. @@ -224,6 +233,7 @@ var ( "onchain.aptos.deployer_key": {"ONCHAIN_APTOS_DEPLOYER_KEY", "APTOS_DEPLOYER_KEY"}, "onchain.tron.deployer_key": {"ONCHAIN_TRON_DEPLOYER_KEY", "TRON_DEPLOYER_KEY"}, "onchain.sui.deployer_key": {"ONCHAIN_SUI_DEPLOYER_KEY", "SUI_DEPLOYER_KEY"}, + "onchain.stellar.deployer_key": {"ONCHAIN_STELLAR_DEPLOYER_KEY"}, "onchain.ton.deployer_key": {"ONCHAIN_TON_DEPLOYER_KEY", "TON_DEPLOYER_KEY"}, "onchain.ton.wallet_version": {"ONCHAIN_TON_WALLET_VERSION", "TON_WALLET_VERSION"}, "offchain.job_distributor.auth.cognito_app_client_id": {"OFFCHAIN_JD_AUTH_COGNITO_APP_CLIENT_ID", "JD_AUTH_COGNITO_APP_CLIENT_ID"}, diff --git a/engine/cld/config/env/config_test.go b/engine/cld/config/env/config_test.go index a365e3c8..da15c57c 100644 --- a/engine/cld/config/env/config_test.go +++ b/engine/cld/config/env/config_test.go @@ -41,6 +41,9 @@ var ( DeployerKey: "0xedd", WalletVersion: "V5R1", }, + Stellar: StellarConfig{ + DeployerKey: "0x567", + }, }, Offchain: OffchainConfig{ JobDistributor: JobDistributorConfig{ @@ -81,6 +84,7 @@ var ( "ONCHAIN_APTOS_DEPLOYER_KEY": "0x123", "ONCHAIN_TRON_DEPLOYER_KEY": "0x123", "ONCHAIN_SUI_DEPLOYER_KEY": "0x123", + "ONCHAIN_STELLAR_DEPLOYER_KEY": "0x567", "ONCHAIN_TON_DEPLOYER_KEY": "0x123", "ONCHAIN_TON_WALLET_VERSION": "V5R1", "OFFCHAIN_JD_AUTH_COGNITO_APP_CLIENT_ID": "123", @@ -118,9 +122,10 @@ var ( "TON_DEPLOYER_KEY": "0x123", "TON_WALLET_VERSION": "V5R1", // These values do not have a legacy equivalent - "CATALOG_GRPC": "http://localhost:8080", - "CATALOG_AUTH_KMS_KEY_ID": "123", - "CATALOG_AUTH_KMS_KEY_REGION": "us-east-1", + "ONCHAIN_STELLAR_DEPLOYER_KEY": "0x567", // Stellar is new, uses new-style env var + "CATALOG_GRPC": "http://localhost:8080", + "CATALOG_AUTH_KMS_KEY_ID": "123", + "CATALOG_AUTH_KMS_KEY_REGION": "us-east-1", } // envCfg is the config that is loaded from the environment variables. @@ -150,6 +155,9 @@ var ( Sui: SuiConfig{ DeployerKey: "0x123", }, + Stellar: StellarConfig{ + DeployerKey: "0x567", + }, Ton: TonConfig{ DeployerKey: "0x123", WalletVersion: "V5R1", @@ -205,10 +213,12 @@ func Test_Load(t *testing.T) { //nolint:paralleltest // see comment in setupTest EVM: EVMConfig{ Seth: nil, // Testing optional pointer fields }, - Solana: SolanaConfig{}, - Aptos: AptosConfig{}, - Tron: TronConfig{}, - Ton: TonConfig{}, + Solana: SolanaConfig{}, + Aptos: AptosConfig{}, + Tron: TronConfig{}, + Sui: SuiConfig{}, + Ton: TonConfig{}, + Stellar: StellarConfig{}, }, Offchain: OffchainConfig{ JobDistributor: JobDistributorConfig{ diff --git a/engine/cld/config/env/testdata/config.yml b/engine/cld/config/env/testdata/config.yml index bed68f0c..a39ccf8f 100644 --- a/engine/cld/config/env/testdata/config.yml +++ b/engine/cld/config/env/testdata/config.yml @@ -22,6 +22,8 @@ onchain: ton: deployer_key: "0xedd" wallet_version: "V5R1" + stellar: + deployer_key: "0x567" offchain: job_distributor: endpoints: diff --git a/engine/cld/config/env/testdata/config_with_optional_values.yml b/engine/cld/config/env/testdata/config_with_optional_values.yml index e42905d8..e9c849c3 100644 --- a/engine/cld/config/env/testdata/config_with_optional_values.yml +++ b/engine/cld/config/env/testdata/config_with_optional_values.yml @@ -17,6 +17,8 @@ onchain: ton: deployer_key: "0xedd" wallet_version: "V5R1" + stellar: + deployer_key: "0x567" offchain: job_distributor: endpoints: diff --git a/engine/cld/config/env_test.go b/engine/cld/config/env_test.go index f8ed9f87..9fbc68dd 100644 --- a/engine/cld/config/env_test.go +++ b/engine/cld/config/env_test.go @@ -42,6 +42,7 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are assert.Equal(t, "0xdef", cfg.Onchain.Tron.DeployerKey) assert.Equal(t, "f1a2b3c4", cfg.Onchain.KMS.KeyID) assert.Equal(t, "us-west-1", cfg.Onchain.KMS.KeyRegion) + assert.Equal(t, "0x567", cfg.Onchain.Stellar.DeployerKey) }, }, { @@ -116,6 +117,7 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are "ONCHAIN_APTOS_DEPLOYER_KEY": "0x345", "ONCHAIN_TRON_DEPLOYER_KEY": "0x456", "ONCHAIN_SUI_DEPLOYER_KEY": "0x567", + "ONCHAIN_STELLAR_DEPLOYER_KEY": "0x567", "ONCHAIN_GETH_WRAPPERS_DIRS": "dir1,dir2", "ONCHAIN_SETH_CONFIG_FILE": "/tmp/config", }, @@ -145,6 +147,7 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are assert.Equal(t, "http://localhost:2000", cfg.Catalog.GRPC) assert.Equal(t, "c4f1a2b3", cfg.Catalog.Auth.KMSKeyID) assert.Equal(t, "us-east-1", cfg.Catalog.Auth.KMSKeyRegion) + assert.Equal(t, "0x567", cfg.Onchain.Stellar.DeployerKey) }, }, } diff --git a/engine/cld/config/network/metadata.go b/engine/cld/config/network/metadata.go index e639f285..e206c364 100644 --- a/engine/cld/config/network/metadata.go +++ b/engine/cld/config/network/metadata.go @@ -12,6 +12,13 @@ type EVMMetadata struct { AnvilConfig *AnvilConfig `yaml:"anvil_config,omitempty"` } +// StellarMetadata holds metadata specific to Stellar networks. +// The main RPC URL comes from network.RPCs (like other chains); only passphrase and Friendbot (faucet) URL live here. +type StellarMetadata struct { + NetworkPassphrase string `yaml:"network_passphrase"` + FriendbotURL string `yaml:"friendbot_url"` +} + // AnvilConfig holds the configuration for starting an Anvil node. type AnvilConfig struct { Image string `yaml:"image"` diff --git a/go.mod b/go.mod index fa0f7274..1644c5b7 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 github.com/segmentio/ksuid v1.0.4 github.com/smartcontractkit/ccip-owner-contracts v0.1.0 - github.com/smartcontractkit/chain-selectors v1.0.91 + github.com/smartcontractkit/chain-selectors v1.0.94 github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260129103204-4c8453dd8139 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260129103204-4c8453dd8139 @@ -45,6 +45,7 @@ require ( github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 + github.com/stellar/go-stellar-sdk v0.1.0 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.39.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0 @@ -65,6 +66,8 @@ require ( github.com/btcsuite/btcutil v1.0.2 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/creachadair/jrpc2 v1.2.0 // indirect + github.com/creachadair/mds v0.13.4 // indirect github.com/dchest/siphash v1.2.3 // indirect github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect @@ -78,6 +81,7 @@ require ( github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20251124151448-0448aefdaab9 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect + github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index abcca4f6..33580c48 100644 --- a/go.sum +++ b/go.sum @@ -180,6 +180,10 @@ github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/creachadair/jrpc2 v1.2.0 h1:SXr0OgnwM0X18P+HccJP0uT3KGSDk/BCSRlJBvE2bMY= +github.com/creachadair/jrpc2 v1.2.0/go.mod h1:66uKSdr6tR5ZeNvkIjDSbbVUtOv0UhjS/vcd8ECP7Iw= +github.com/creachadair/mds v0.13.4 h1:RgU0MhiVqkzp6/xtNWhK6Pw7tDeaVuGFtA0UA2RBYvY= +github.com/creachadair/mds v0.13.4/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -239,6 +243,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -262,8 +268,10 @@ github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8x github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -345,6 +353,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -512,6 +522,8 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE= +github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -675,6 +687,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= +github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc= +github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= @@ -694,8 +708,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9LsA7vTMPv+0n7ClhSFnZFAk= github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= -github.com/smartcontractkit/chain-selectors v1.0.91 h1:Aip7IZTv40RtbHgZ9mTjm5KyhYrpPefG7iVMzLZ27M4= -github.com/smartcontractkit/chain-selectors v1.0.91/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chain-selectors v1.0.94 h1:5oZxl7mrI9wETxPte5NITQRDSR/dZHX4Z4cuclTUNwM= +github.com/smartcontractkit/chain-selectors v1.0.94/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2 h1:vGdeMwHO3ow88HvxfhA4DDPYNY0X9jmdux7L83UF/W8= github.com/smartcontractkit/chainlink-aptos v0.0.0-20251024142440-51f2ad2652a2/go.mod h1:iteU0WORHkArACVh/HoY/1bipV4TcNcJdTmom9uIT0E= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260129103204-4c8453dd8139 h1:OeZ/wkrH9pRogFTUE9TrSncYA/xLTj2ui5VmkX0/6ss= @@ -749,6 +763,10 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stellar/go-stellar-sdk v0.1.0 h1:MfV7dv4k6xQQrWeKT7npWyKhjoayphLVGwXKtTLNeH8= +github.com/stellar/go-stellar-sdk v0.1.0/go.mod h1:fZPcxQZw1I0zZ+X76uFcVPqmQCaYbWc87lDFW/kQJaY= +github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 h1:OzCVd0SV5qE3ZcDeSFCmOWLZfEWZ3Oe8KtmSOYKEVWE= +github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863 h1:ba4VRWSkRzgdP5hB5OxexIzBXZbSwgcw8bEu06ivGQI= github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863/go.mod h1:oPTjPNrRucLv9mU27iNPj6n0CWWcNFhoXFOLVGJwHCA= github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= @@ -813,6 +831,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+x github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdrpp/goxdr v0.1.1 h1:E1B2c6E8eYhOVyd7yEpOyopzTPirUeF6mVOfXfGyJyc= +github.com/xdrpp/goxdr v0.1.1/go.mod h1:dXo1scL/l6s7iME1gxHWo2XCppbHEKZS7m/KyYWkNzA= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xssnick/tonutils-go v1.14.1 h1:zV/iVYl/h3hArS+tPsd9XrSFfGert3r21caMltPSeHg=