From 6fd4cfc087ab21d38cee6d42502e566f1efbfb08 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Thu, 5 Feb 2026 15:54:27 -0500 Subject: [PATCH 01/18] add simple rpc provider for Stellar --- chain/stellar/provider/rpc_provider.go | 73 ++++++++++++++++++++++++++ chain/stellar/stellar_chain.go | 12 +++++ 2 files changed, 85 insertions(+) create mode 100644 chain/stellar/provider/rpc_provider.go create mode 100644 chain/stellar/stellar_chain.go diff --git a/chain/stellar/provider/rpc_provider.go b/chain/stellar/provider/rpc_provider.go new file mode 100644 index 00000000..36d13749 --- /dev/null +++ b/chain/stellar/provider/rpc_provider.go @@ -0,0 +1,73 @@ +package provider + +import ( + "context" + "errors" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" +) + +type RPCChainProviderConfig struct { + NetworkPassphrase string + FriendbotURL string + SorobanRPCURL string +} + +func (c RPCChainProviderConfig) validate() error { + if c.NetworkPassphrase == "" { + return errors.New("network passphrase is required") + } + if c.FriendbotURL == "" { + return errors.New("friendbot url is required") + } + if c.SorobanRPCURL == "" { + return errors.New("soroban rpc url 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, err + } + + p.chain = &stellar.Chain{ + ChainMetadata: stellar.ChainMetadata{Selector: p.selector}, + } + + 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/stellar_chain.go b/chain/stellar/stellar_chain.go new file mode 100644 index 00000000..9bbf3447 --- /dev/null +++ b/chain/stellar/stellar_chain.go @@ -0,0 +1,12 @@ +package stellar + +import ( + chaincommon "github.com/smartcontractkit/chainlink-deployments-framework/chain/internal/common" +) + +type ChainMetadata = chaincommon.ChainMetadata + +// Chain represents a Stellar network instance used CLDF. +type Chain struct { + ChainMetadata +} From f6f2ec8b8ec57e140d18e57ee17e7a7e756f0686 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 09:31:49 -0500 Subject: [PATCH 02/18] add a changeset --- .changeset/rich-pumas-cheat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rich-pumas-cheat.md diff --git a/.changeset/rich-pumas-cheat.md b/.changeset/rich-pumas-cheat.md new file mode 100644 index 00000000..821130f5 --- /dev/null +++ b/.changeset/rich-pumas-cheat.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": patch +--- + +add a Stellar RPC provider and chain From 153d42079f7d508a5b2bfbf670d0cd34d0844772 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 09:36:34 -0500 Subject: [PATCH 03/18] Update chain/stellar/stellar_chain.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- chain/stellar/stellar_chain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chain/stellar/stellar_chain.go b/chain/stellar/stellar_chain.go index 9bbf3447..a835a156 100644 --- a/chain/stellar/stellar_chain.go +++ b/chain/stellar/stellar_chain.go @@ -6,7 +6,7 @@ import ( type ChainMetadata = chaincommon.ChainMetadata -// Chain represents a Stellar network instance used CLDF. +// Chain represents a Stellar network instance used by the Chainlink Deployments Framework (CLDF). type Chain struct { ChainMetadata } From 4c2831916a78100c28fed387931e004bebbf8a3d Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 09:37:56 -0500 Subject: [PATCH 04/18] Update chain/stellar/provider/rpc_provider.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- chain/stellar/provider/rpc_provider.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chain/stellar/provider/rpc_provider.go b/chain/stellar/provider/rpc_provider.go index 36d13749..4e669667 100644 --- a/chain/stellar/provider/rpc_provider.go +++ b/chain/stellar/provider/rpc_provider.go @@ -69,5 +69,8 @@ func (p *RPCChainProvider) ChainSelector() uint64 { } func (p *RPCChainProvider) BlockChain() chain.BlockChain { + if p.chain == nil { + return nil + } return *p.chain } From dd9f8b304210b218470a0629911ce20092a070ac Mon Sep 17 00:00:00 2001 From: Faisal Date: Mon, 9 Feb 2026 18:39:51 +0400 Subject: [PATCH 05/18] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- chain/stellar/provider/rpc_provider.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chain/stellar/provider/rpc_provider.go b/chain/stellar/provider/rpc_provider.go index 4e669667..8997af66 100644 --- a/chain/stellar/provider/rpc_provider.go +++ b/chain/stellar/provider/rpc_provider.go @@ -19,10 +19,10 @@ func (c RPCChainProviderConfig) validate() error { return errors.New("network passphrase is required") } if c.FriendbotURL == "" { - return errors.New("friendbot url is required") + return errors.New("Friendbot URL is required") } if c.SorobanRPCURL == "" { - return errors.New("soroban rpc url is required") + return errors.New("Soroban RPC URL is required") } return nil From a68420931e5b1028583dfcd3dbcdb294e8d16efe Mon Sep 17 00:00:00 2001 From: faisal-link Date: Mon, 9 Feb 2026 18:57:20 +0400 Subject: [PATCH 06/18] return references --- chain/stellar/provider/rpc_provider.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chain/stellar/provider/rpc_provider.go b/chain/stellar/provider/rpc_provider.go index 8997af66..b816e52d 100644 --- a/chain/stellar/provider/rpc_provider.go +++ b/chain/stellar/provider/rpc_provider.go @@ -57,7 +57,7 @@ func (p *RPCChainProvider) Initialize(_ context.Context) (chain.BlockChain, erro ChainMetadata: stellar.ChainMetadata{Selector: p.selector}, } - return *p.chain, nil + return p.chain, nil } func (p *RPCChainProvider) Name() string { @@ -72,5 +72,5 @@ func (p *RPCChainProvider) BlockChain() chain.BlockChain { if p.chain == nil { return nil } - return *p.chain + return p.chain } From b30b3afdbba668312923908663b54dc59a8a6bc7 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 11:20:44 -0500 Subject: [PATCH 07/18] add stellar chain support --- chain/blockchain.go | 7 ++++ chain/stellar/provider/rpc_provider.go | 5 +-- engine/cld/chains/chains.go | 43 +++++++++++++++++++++++ engine/cld/config/env/config.go | 24 +++++++++---- engine/cld/config/env/config_test.go | 18 +++++++--- engine/cld/config/env/testdata/config.yml | 2 ++ go.mod | 2 +- go.sum | 4 +-- 8 files changed, 89 insertions(+), 16 deletions(-) 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/stellar/provider/rpc_provider.go b/chain/stellar/provider/rpc_provider.go index b816e52d..210d1b8a 100644 --- a/chain/stellar/provider/rpc_provider.go +++ b/chain/stellar/provider/rpc_provider.go @@ -19,10 +19,10 @@ func (c RPCChainProviderConfig) validate() error { return errors.New("network passphrase is required") } if c.FriendbotURL == "" { - return errors.New("Friendbot URL is required") + return errors.New("friendbot URL is required") } if c.SorobanRPCURL == "" { - return errors.New("Soroban RPC URL is required") + return errors.New("soroban RPC URL is required") } return nil @@ -72,5 +72,6 @@ func (p *RPCChainProvider) BlockChain() chain.BlockChain { if p.chain == nil { return nil } + return p.chain } diff --git a/engine/cld/chains/chains.go b/engine/cld/chains/chains.go index 69c3fa4a..dee1214f 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,41 @@ 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. +func (l *chainLoaderStellar) Load(ctx context.Context, selector uint64) (fchain.BlockChain, error) { + network, err := l.getNetwork(selector) + if err != nil { + return nil, err + } + + c, err := stellarprov.NewRPCChainProvider(selector, + stellarprov.RPCChainProviderConfig{ + NetworkPassphrase: network.Metadata.(map[string]any)["network_passphrase"].(string), + FriendbotURL: network.RPCs[1].HTTPURL, + SorobanRPCURL: network.RPCs[0].HTTPURL, + }, + ).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/config/env/config.go b/engine/cld/config/env/config.go index 71d2cd58..cd73fbce 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", "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..6b906619 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", @@ -107,6 +111,7 @@ var ( "APTOS_DEPLOYER_KEY": "0x123", "SUI_DEPLOYER_KEY": "0x123", "TRON_DEPLOYER_KEY": "0x123", + "STELLAR_DEPLOYER_KEY": "0x567", "JD_AUTH_COGNITO_APP_CLIENT_ID": "123", "JD_AUTH_COGNITO_APP_CLIENT_SECRET": "123", "JD_AUTH_AWS_REGION": "us-east-1", @@ -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/go.mod b/go.mod index fa0f7274..a6191e4a 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.93 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 diff --git a/go.sum b/go.sum index abcca4f6..1d9be752 100644 --- a/go.sum +++ b/go.sum @@ -694,8 +694,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.93 h1:nJy9PAj9oShZW/zR21LvqNApJXlZ8TLXVWtJx4rbIio= +github.com/smartcontractkit/chain-selectors v1.0.93/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= From d220292778565420f2fd40cf366a8ca1e717d9cf Mon Sep 17 00:00:00 2001 From: ajaskolski Date: Mon, 9 Feb 2026 19:35:58 +0100 Subject: [PATCH 08/18] stellar adds rpcs by name in metadat --- engine/cld/chains/chains.go | 17 ++++++++++++++--- engine/cld/config/network/metadata.go | 7 +++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/engine/cld/chains/chains.go b/engine/cld/chains/chains.go index dee1214f..901f3482 100644 --- a/engine/cld/chains/chains.go +++ b/engine/cld/chains/chains.go @@ -347,17 +347,28 @@ func newChainLoaderStellar( } // 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: network.Metadata.(map[string]any)["network_passphrase"].(string), - FriendbotURL: network.RPCs[1].HTTPURL, - SorobanRPCURL: network.RPCs[0].HTTPURL, + NetworkPassphrase: md.NetworkPassphrase, + FriendbotURL: md.FriendbotURL, + SorobanRPCURL: rpcURL, }, ).Initialize(ctx) if err != nil { 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"` From 39b8ca2349b7e238e963f4e2955803a05b39d24c Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 15:20:04 -0500 Subject: [PATCH 09/18] add support for stellar rpc provider --- chain/blockchain_test.go | 48 +++- chain/stellar/provider/rpc_provider_test.go | 261 ++++++++++++++++++++ engine/cld/chains/chains_test.go | 238 +++++++++++++++++- go.mod | 2 +- go.sum | 4 +- 5 files changed, 533 insertions(+), 20 deletions(-) create mode 100644 chain/stellar/provider/rpc_provider_test.go 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/stellar/provider/rpc_provider_test.go b/chain/stellar/provider/rpc_provider_test.go new file mode 100644 index 00000000..f5d157fd --- /dev/null +++ b/chain/stellar/provider/rpc_provider_test.go @@ -0,0 +1,261 @@ +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_RPCChainProviderConfig_validate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + config RPCChainProviderConfig + wantErr string + }{ + { + name: "valid config", + config: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + }, + wantErr: "", + }, + { + name: "missing network passphrase", + config: RPCChainProviderConfig{ + NetworkPassphrase: "", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + }, + wantErr: "network passphrase is required", + }, + { + name: "missing friendbot URL", + config: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + }, + wantErr: "friendbot URL is required", + }, + { + name: "missing soroban RPC URL", + config: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "", + }, + wantErr: "soroban RPC URL is required", + }, + { + name: "all fields missing", + config: RPCChainProviderConfig{ + NetworkPassphrase: "", + FriendbotURL: "", + SorobanRPCURL: "", + }, + wantErr: "network passphrase is required", + }, + } + + 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_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", + }, + }, + { + 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", + }, + 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", + }, + wantErr: "network passphrase is required", + }, + { + name: "fails config validation - missing friendbot URL", + giveSelector: 12345, + giveConfig: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + }, + wantErr: "friendbot URL is required", + }, + { + name: "fails config validation - missing soroban RPC URL", + giveSelector: 12345, + giveConfig: RPCChainProviderConfig{ + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "", + }, + wantErr: "soroban RPC URL 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 + if tt.giveChain != nil { + assert.Equal(t, tt.giveChain, gotChain) + } + }) + } +} + +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", + } + + 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/engine/cld/chains/chains_test.go b/engine/cld/chains/chains_test.go index 250ebb4d..f20fc525 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: "0x123467890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, } 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() + + var stellarSelector uint64 = 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: "0x123467890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, + } + + 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", + 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 + }, + }, + }), + giveOnchainConfig: onchainConfig, + wantErr: "friendbot URL is required", + }, + // 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/go.mod b/go.mod index a6191e4a..eacb8f96 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.93 + 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 diff --git a/go.sum b/go.sum index 1d9be752..ad1b045d 100644 --- a/go.sum +++ b/go.sum @@ -694,8 +694,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.93 h1:nJy9PAj9oShZW/zR21LvqNApJXlZ8TLXVWtJx4rbIio= -github.com/smartcontractkit/chain-selectors v1.0.93/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= From 2647335502b956304da2a7747fb28e435d8417a8 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 15:56:35 -0500 Subject: [PATCH 10/18] fix tests and lint --- engine/cld/chains/chains_test.go | 2 +- engine/cld/config/env/testdata/config_with_optional_values.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/cld/chains/chains_test.go b/engine/cld/chains/chains_test.go index f20fc525..3a30042e 100644 --- a/engine/cld/chains/chains_test.go +++ b/engine/cld/chains/chains_test.go @@ -595,7 +595,7 @@ func Test_chainLoaderSui_Load(t *testing.T) { func Test_chainLoaderStellar_Load(t *testing.T) { t.Parallel() - var stellarSelector uint64 = chainsel.STELLAR_TESTNET.Selector + stellarSelector := chainsel.STELLAR_TESTNET.Selector networkCfg := cfgnet.NewConfig([]cfgnet.Network{ { 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: From 66b7ca1e4ea903872e2d89f45e8d1a7e9e92cc1e Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 16:31:09 -0500 Subject: [PATCH 11/18] enhance chain struct --- chain/stellar/provider/keypair_generator.go | 61 ++++ .../provider/keypair_generator_test.go | 211 ++++++++++++ chain/stellar/provider/rpc_provider.go | 49 ++- chain/stellar/provider/rpc_provider_test.go | 116 ++++--- chain/stellar/signer.go | 77 +++++ chain/stellar/signer_test.go | 321 ++++++++++++++++++ chain/stellar/stellar_chain.go | 17 + engine/cld/chains/chains.go | 7 +- engine/cld/chains/chains_test.go | 8 +- engine/cld/config/env_test.go | 5 + go.mod | 4 + go.sum | 24 +- 12 files changed, 844 insertions(+), 56 deletions(-) create mode 100644 chain/stellar/provider/keypair_generator.go create mode 100644 chain/stellar/provider/keypair_generator_test.go create mode 100644 chain/stellar/signer.go create mode 100644 chain/stellar/signer_test.go diff --git a/chain/stellar/provider/keypair_generator.go b/chain/stellar/provider/keypair_generator.go new file mode 100644 index 00000000..94c5a2a9 --- /dev/null +++ b/chain/stellar/provider/keypair_generator.go @@ -0,0 +1,61 @@ +package provider + +import ( + "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, fmt.Errorf("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..9233ba6d --- /dev/null +++ b/chain/stellar/provider/keypair_generator_test.go @@ -0,0 +1,211 @@ +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 i := 0; i < 10; i++ { + 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.Equal(t, 10, len(addresses), "should have generated 10 unique addresses") +} + +func TestKeypairGenerator_Interface(t *testing.T) { + t.Parallel() + + // Verify both types implement KeypairGenerator + var _ KeypairGenerator = (*keypairFromHex)(nil) + var _ KeypairGenerator = (*keypairRandom)(nil) + + // Test interface methods + hexGen := KeypairFromHex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + var gen1 KeypairGenerator = hexGen + signer1, err := gen1.Generate() + require.NoError(t, err) + assert.NotNil(t, signer1) + + randomGen := KeypairRandom() + var gen2 KeypairGenerator = randomGen + signer2, err := gen2.Generate() + require.NoError(t, err) + assert.NotNil(t, signer2) +} + +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 index 210d1b8a..7c598535 100644 --- a/chain/stellar/provider/rpc_provider.go +++ b/chain/stellar/provider/rpc_provider.go @@ -3,26 +3,41 @@ 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 - FriendbotURL string - SorobanRPCURL 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") } - if c.FriendbotURL == "" { - return errors.New("friendbot URL is required") - } - if c.SorobanRPCURL == "" { - return errors.New("soroban RPC URL 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 @@ -50,11 +65,27 @@ func (p *RPCChainProvider) Initialize(_ context.Context) (chain.BlockChain, erro } if err := p.config.validate(); err != nil { - return nil, err + 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}, + 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 diff --git a/chain/stellar/provider/rpc_provider_test.go b/chain/stellar/provider/rpc_provider_test.go index f5d157fd..3d436da2 100644 --- a/chain/stellar/provider/rpc_provider_test.go +++ b/chain/stellar/provider/rpc_provider_test.go @@ -21,47 +21,62 @@ func Test_RPCChainProviderConfig_validate(t *testing.T) { { name: "valid config", config: RPCChainProviderConfig{ - NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "https://friendbot.stellar.org", - SorobanRPCURL: "https://soroban-testnet.stellar.org", + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), }, wantErr: "", }, { name: "missing network passphrase", config: RPCChainProviderConfig{ - NetworkPassphrase: "", - FriendbotURL: "https://friendbot.stellar.org", - SorobanRPCURL: "https://soroban-testnet.stellar.org", + NetworkPassphrase: "", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), }, wantErr: "network passphrase is required", }, { - name: "missing friendbot URL", + name: "missing friendbot URL - allowed since optional", config: RPCChainProviderConfig{ - NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "", - SorobanRPCURL: "https://soroban-testnet.stellar.org", + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), }, - wantErr: "friendbot URL is required", + wantErr: "", // FriendbotURL is optional }, { name: "missing soroban RPC URL", config: RPCChainProviderConfig{ - NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "https://friendbot.stellar.org", - SorobanRPCURL: "", + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "", + DeployerKeypairGen: KeypairRandom(), }, wantErr: "soroban RPC URL is required", }, { name: "all fields missing", config: RPCChainProviderConfig{ - NetworkPassphrase: "", - FriendbotURL: "", - SorobanRPCURL: "", + NetworkPassphrase: "", + FriendbotURL: "", + SorobanRPCURL: "", + DeployerKeypairGen: nil, }, - wantErr: "network passphrase is required", + wantErr: "soroban RPC URL is required", + }, + { + name: "missing deployer keypair generator", + config: 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", }, } @@ -93,18 +108,20 @@ func Test_RPCChainProvider_Initialize(t *testing.T) { name: "valid initialization", giveSelector: 12345, giveConfig: RPCChainProviderConfig{ - NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "https://friendbot.stellar.org", - SorobanRPCURL: "https://soroban-testnet.stellar.org", + 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", + 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}, @@ -114,32 +131,46 @@ func Test_RPCChainProvider_Initialize(t *testing.T) { name: "fails config validation - missing network passphrase", giveSelector: 12345, giveConfig: RPCChainProviderConfig{ - NetworkPassphrase: "", - FriendbotURL: "https://friendbot.stellar.org", - SorobanRPCURL: "https://soroban-testnet.stellar.org", + NetworkPassphrase: "", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), }, wantErr: "network passphrase is required", }, { - name: "fails config validation - missing friendbot URL", + name: "missing friendbot URL - allowed since optional", giveSelector: 12345, giveConfig: RPCChainProviderConfig{ - NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "", - SorobanRPCURL: "https://soroban-testnet.stellar.org", + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), }, - wantErr: "friendbot URL is required", + 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: "", + 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 { @@ -165,8 +196,16 @@ func Test_RPCChainProvider_Initialize(t *testing.T) { 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) } }) } @@ -245,9 +284,10 @@ func Test_NewRPCChainProvider(t *testing.T) { selector := uint64(12345) config := RPCChainProviderConfig{ - NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "https://friendbot.stellar.org", - SorobanRPCURL: "https://soroban-testnet.stellar.org", + NetworkPassphrase: "Test SDF Network ; September 2015", + FriendbotURL: "https://friendbot.stellar.org", + SorobanRPCURL: "https://soroban-testnet.stellar.org", + DeployerKeypairGen: KeypairRandom(), } p := NewRPCChainProvider(selector, config) 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..0ce36a7d --- /dev/null +++ b/chain/stellar/signer_test.go @@ -0,0 +1,321 @@ +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) + assert.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) + assert.NoError(t, err) + + err = kp2.Verify(message, sig1) + assert.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) + assert.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) + + var signer StellarSigner = 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.Equal(t, 64, len(hexKey), "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 index a835a156..a35c4d3b 100644 --- a/chain/stellar/stellar_chain.go +++ b/chain/stellar/stellar_chain.go @@ -1,6 +1,8 @@ package stellar import ( + "github.com/stellar/go-stellar-sdk/clients/rpcclient" + chaincommon "github.com/smartcontractkit/chainlink-deployments-framework/chain/internal/common" ) @@ -9,4 +11,19 @@ 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 901f3482..9b3259fb 100644 --- a/engine/cld/chains/chains.go +++ b/engine/cld/chains/chains.go @@ -366,9 +366,10 @@ func (l *chainLoaderStellar) Load(ctx context.Context, selector uint64) (fchain. c, err := stellarprov.NewRPCChainProvider(selector, stellarprov.RPCChainProviderConfig{ - NetworkPassphrase: md.NetworkPassphrase, - FriendbotURL: md.FriendbotURL, - SorobanRPCURL: rpcURL, + NetworkPassphrase: md.NetworkPassphrase, + FriendbotURL: md.FriendbotURL, + SorobanRPCURL: rpcURL, + DeployerKeypairGen: stellarprov.KeypairFromHex(l.cfg.Stellar.DeployerKey), }, ).Initialize(ctx) if err != nil { diff --git a/engine/cld/chains/chains_test.go b/engine/cld/chains/chains_test.go index 3a30042e..9bb9ab69 100644 --- a/engine/cld/chains/chains_test.go +++ b/engine/cld/chains/chains_test.go @@ -630,7 +630,7 @@ func Test_chainLoaderStellar_Load(t *testing.T) { onchainConfig := cfgenv.OnchainConfig{ Stellar: cfgenv.StellarConfig{ - DeployerKey: "0x123467890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + DeployerKey: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", }, } @@ -737,7 +737,7 @@ func Test_chainLoaderStellar_Load(t *testing.T) { wantErr: "network passphrase is required", }, { - name: "missing friendbot URL in metadata", + name: "missing friendbot URL in metadata - allowed since optional", giveSelector: 333333, giveNetworkConfig: cfgnet.NewConfig([]cfgnet.Network{ { @@ -753,12 +753,12 @@ func Test_chainLoaderStellar_Load(t *testing.T) { }, Metadata: cfgnet.StellarMetadata{ NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "", // Empty friendbot URL + FriendbotURL: "", // Empty friendbot URL (optional) }, }, }), giveOnchainConfig: onchainConfig, - wantErr: "friendbot URL is required", + wantErr: "", // FriendbotURL is optional }, // Note: Empty deployer key test is omitted because Stellar RPC provider // doesn't validate the deployer key at initialization time. diff --git a/engine/cld/config/env_test.go b/engine/cld/config/env_test.go index f8ed9f87..ae36e32c 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) }, }, { @@ -66,6 +67,7 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are "SOLANA_PROGRAM_PATH": "0xcde", "APTOS_DEPLOYER_KEY": "0x345", "TRON_DEPLOYER_KEY": "0x456", + "STELLAR_DEPLOYER_KEY": "0x567", }, wantFunc: func(t *testing.T, cfg *cfgenv.Config) { t.Helper() @@ -89,6 +91,7 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are assert.Equal(t, "0x234", cfg.Onchain.Solana.WalletKey) assert.Equal(t, "0x345", cfg.Onchain.Aptos.DeployerKey) assert.Equal(t, "0x456", cfg.Onchain.Tron.DeployerKey) + assert.Equal(t, "0x567", cfg.Onchain.Stellar.DeployerKey) }, }, { @@ -116,6 +119,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 +149,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/go.mod b/go.mod index eacb8f96..1644c5b7 100644 --- a/go.mod +++ b/go.mod @@ -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 ad1b045d..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= @@ -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= From 5eac1fba801820f3393b7772e89448338822de55 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 16:34:10 -0500 Subject: [PATCH 12/18] add chain access for mcms --- chain/mcms/adapters/chain_access.go | 13 +++++++++++++ chain/mcms/adapters/chain_access_test.go | 19 +++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) 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) } From 21abc9022902b363fd1f1c138172eaca53461e55 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 16:46:39 -0500 Subject: [PATCH 13/18] small fixes --- .github/workflows/pull-request-main.yml | 22 ++++++++++++++++--- chain/stellar/provider/keypair_generator.go | 3 ++- .../provider/keypair_generator_test.go | 9 ++++---- chain/stellar/signer_test.go | 14 +++++++----- 4 files changed, 34 insertions(+), 14 deletions(-) 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/stellar/provider/keypair_generator.go b/chain/stellar/provider/keypair_generator.go index 94c5a2a9..61118e58 100644 --- a/chain/stellar/provider/keypair_generator.go +++ b/chain/stellar/provider/keypair_generator.go @@ -1,6 +1,7 @@ package provider import ( + "errors" "fmt" "github.com/stellar/go-stellar-sdk/keypair" @@ -29,7 +30,7 @@ func KeypairFromHex(hexKey string) KeypairGenerator { // Generate generates a Stellar keypair from the hex-encoded private key. func (k *keypairFromHex) Generate() (stellar.StellarSigner, error) { if k.hexKey == "" { - return nil, fmt.Errorf("hex key is empty") + return nil, errors.New("hex key is empty") } kp, err := stellar.KeypairFromHex(k.hexKey) diff --git a/chain/stellar/provider/keypair_generator_test.go b/chain/stellar/provider/keypair_generator_test.go index 9233ba6d..799c158a 100644 --- a/chain/stellar/provider/keypair_generator_test.go +++ b/chain/stellar/provider/keypair_generator_test.go @@ -52,6 +52,7 @@ func TestKeypairFromHex_Generate(t *testing.T) { if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) assert.Nil(t, signer) + return } @@ -115,7 +116,7 @@ func TestKeypairRandom_GenerateUnique(t *testing.T) { // Generate multiple times addresses := make(map[string]bool) - for i := 0; i < 10; i++ { + for range 10 { signer, err := gen.Generate() require.NoError(t, err) @@ -127,7 +128,7 @@ func TestKeypairRandom_GenerateUnique(t *testing.T) { addresses[address] = true } - assert.Equal(t, 10, len(addresses), "should have generated 10 unique addresses") + assert.Len(t, addresses, 10, "should have generated 10 unique addresses") } func TestKeypairGenerator_Interface(t *testing.T) { @@ -139,13 +140,13 @@ func TestKeypairGenerator_Interface(t *testing.T) { // Test interface methods hexGen := KeypairFromHex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - var gen1 KeypairGenerator = hexGen + gen1 := hexGen signer1, err := gen1.Generate() require.NoError(t, err) assert.NotNil(t, signer1) randomGen := KeypairRandom() - var gen2 KeypairGenerator = randomGen + gen2 := randomGen signer2, err := gen2.Generate() require.NoError(t, err) assert.NotNil(t, signer2) diff --git a/chain/stellar/signer_test.go b/chain/stellar/signer_test.go index 0ce36a7d..de547153 100644 --- a/chain/stellar/signer_test.go +++ b/chain/stellar/signer_test.go @@ -118,6 +118,7 @@ func TestKeypairFromHex(t *testing.T) { if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) assert.Nil(t, kp) + return } @@ -132,7 +133,7 @@ func TestKeypairFromHex(t *testing.T) { // Verify the signature err = kp.Verify(message, sig) - assert.NoError(t, err) + require.NoError(t, err) }) } } @@ -167,10 +168,10 @@ func TestKeypairFromHex_ConsistentAddress(t *testing.T) { // Each keypair should be able to verify the other's signature err = kp1.Verify(message, sig2) - assert.NoError(t, err) + require.NoError(t, err) err = kp2.Verify(message, sig1) - assert.NoError(t, err) + require.NoError(t, err) } func TestKeypairFromHex_RealStellarKey(t *testing.T) { @@ -188,7 +189,7 @@ func TestKeypairFromHex_RealStellarKey(t *testing.T) { require.NoError(t, err) err = reconstructedKp.Verify(message, sig) - assert.NoError(t, err) + require.NoError(t, err) // Verify the address is a valid Stellar address format (starts with G) address := reconstructedKp.Address() @@ -221,7 +222,7 @@ func TestStellarSigner_Interface(t *testing.T) { kp, err := keypair.Random() require.NoError(t, err) - var signer StellarSigner = NewStellarKeypairSigner(kp) + signer := NewStellarKeypairSigner(kp) require.NotNil(t, signer) // Test all interface methods @@ -278,6 +279,7 @@ func TestKeypairFromHex_EdgeCases(t *testing.T) { if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) assert.Nil(t, kp) + return } @@ -305,7 +307,7 @@ func TestKeypairFromHex_ByteConversion(t *testing.T) { } hexKey := hex.EncodeToString(expectedBytes) - require.Equal(t, 64, len(hexKey), "hex encoding of 32 bytes should be 64 chars") + require.Len(t, hexKey, 64, "hex encoding of 32 bytes should be 64 chars") kp, err := KeypairFromHex(hexKey) require.NoError(t, err) From 18faf9eb880debba04afb67cfae69dcf9c6eaf92 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 16:50:42 -0500 Subject: [PATCH 14/18] fix a test --- engine/cld/chains/chains_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/cld/chains/chains_test.go b/engine/cld/chains/chains_test.go index 9bb9ab69..fa8c032b 100644 --- a/engine/cld/chains/chains_test.go +++ b/engine/cld/chains/chains_test.go @@ -279,7 +279,7 @@ func Test_LoadChains(t *testing.T) { WalletVersion: "V4R2", }, Stellar: cfgenv.StellarConfig{ - DeployerKey: "0x123467890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + DeployerKey: "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", }, } From b0e7036dbb456a5477c0d14fee4fbb7bf2433142 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 16:59:47 -0500 Subject: [PATCH 15/18] update changeset --- .changeset/rich-pumas-cheat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rich-pumas-cheat.md b/.changeset/rich-pumas-cheat.md index 821130f5..00024c4a 100644 --- a/.changeset/rich-pumas-cheat.md +++ b/.changeset/rich-pumas-cheat.md @@ -2,4 +2,4 @@ "chainlink-deployments-framework": patch --- -add a Stellar RPC provider and chain +add Stellar support for mcms adapters, RPC provider, chain config, and blockchain From 673922b9392bdd0476dec37c22da41a4448f66af Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 22:13:15 -0500 Subject: [PATCH 16/18] update --- .../provider/keypair_generator_test.go | 21 ----- chain/stellar/provider/rpc_provider.go | 4 - chain/stellar/provider/rpc_provider_test.go | 84 ------------------- engine/cld/config/env/config.go | 2 +- 4 files changed, 1 insertion(+), 110 deletions(-) diff --git a/chain/stellar/provider/keypair_generator_test.go b/chain/stellar/provider/keypair_generator_test.go index 799c158a..3959552a 100644 --- a/chain/stellar/provider/keypair_generator_test.go +++ b/chain/stellar/provider/keypair_generator_test.go @@ -131,27 +131,6 @@ func TestKeypairRandom_GenerateUnique(t *testing.T) { assert.Len(t, addresses, 10, "should have generated 10 unique addresses") } -func TestKeypairGenerator_Interface(t *testing.T) { - t.Parallel() - - // Verify both types implement KeypairGenerator - var _ KeypairGenerator = (*keypairFromHex)(nil) - var _ KeypairGenerator = (*keypairRandom)(nil) - - // Test interface methods - hexGen := KeypairFromHex("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - gen1 := hexGen - signer1, err := gen1.Generate() - require.NoError(t, err) - assert.NotNil(t, signer1) - - randomGen := KeypairRandom() - gen2 := randomGen - signer2, err := gen2.Generate() - require.NoError(t, err) - assert.NotNil(t, signer2) -} - func TestKeypairFromHex_GenerateWithDifferentKeys(t *testing.T) { t.Parallel() diff --git a/chain/stellar/provider/rpc_provider.go b/chain/stellar/provider/rpc_provider.go index 7c598535..96bc37fa 100644 --- a/chain/stellar/provider/rpc_provider.go +++ b/chain/stellar/provider/rpc_provider.go @@ -100,9 +100,5 @@ func (p *RPCChainProvider) ChainSelector() uint64 { } func (p *RPCChainProvider) BlockChain() chain.BlockChain { - if p.chain == nil { - return nil - } - return p.chain } diff --git a/chain/stellar/provider/rpc_provider_test.go b/chain/stellar/provider/rpc_provider_test.go index 3d436da2..c13cfc73 100644 --- a/chain/stellar/provider/rpc_provider_test.go +++ b/chain/stellar/provider/rpc_provider_test.go @@ -10,90 +10,6 @@ import ( "github.com/smartcontractkit/chainlink-deployments-framework/chain/stellar" ) -func Test_RPCChainProviderConfig_validate(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - config RPCChainProviderConfig - wantErr string - }{ - { - name: "valid config", - config: RPCChainProviderConfig{ - NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "https://friendbot.stellar.org", - SorobanRPCURL: "https://soroban-testnet.stellar.org", - DeployerKeypairGen: KeypairRandom(), - }, - wantErr: "", - }, - { - name: "missing network passphrase", - config: 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", - config: RPCChainProviderConfig{ - NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "", - SorobanRPCURL: "https://soroban-testnet.stellar.org", - DeployerKeypairGen: KeypairRandom(), - }, - wantErr: "", // FriendbotURL is optional - }, - { - name: "missing soroban RPC URL", - config: RPCChainProviderConfig{ - NetworkPassphrase: "Test SDF Network ; September 2015", - FriendbotURL: "https://friendbot.stellar.org", - SorobanRPCURL: "", - DeployerKeypairGen: KeypairRandom(), - }, - wantErr: "soroban RPC URL is required", - }, - { - name: "all fields missing", - config: RPCChainProviderConfig{ - NetworkPassphrase: "", - FriendbotURL: "", - SorobanRPCURL: "", - DeployerKeypairGen: nil, - }, - wantErr: "soroban RPC URL is required", - }, - { - name: "missing deployer keypair generator", - config: 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() - - err := tt.config.validate() - if tt.wantErr != "" { - require.ErrorContains(t, err, tt.wantErr) - } else { - require.NoError(t, err) - } - }) - } -} - func Test_RPCChainProvider_Initialize(t *testing.T) { t.Parallel() diff --git a/engine/cld/config/env/config.go b/engine/cld/config/env/config.go index cd73fbce..0df09b09 100644 --- a/engine/cld/config/env/config.go +++ b/engine/cld/config/env/config.go @@ -233,7 +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", "STELLAR_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"}, From 77846e5bd856bf8f5c169741eec352f77d819c5d Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 22:28:42 -0500 Subject: [PATCH 17/18] update --- engine/cld/config/env/config_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/engine/cld/config/env/config_test.go b/engine/cld/config/env/config_test.go index 6b906619..da15c57c 100644 --- a/engine/cld/config/env/config_test.go +++ b/engine/cld/config/env/config_test.go @@ -111,7 +111,6 @@ var ( "APTOS_DEPLOYER_KEY": "0x123", "SUI_DEPLOYER_KEY": "0x123", "TRON_DEPLOYER_KEY": "0x123", - "STELLAR_DEPLOYER_KEY": "0x567", "JD_AUTH_COGNITO_APP_CLIENT_ID": "123", "JD_AUTH_COGNITO_APP_CLIENT_SECRET": "123", "JD_AUTH_AWS_REGION": "us-east-1", @@ -123,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. From da90030ac0760a6234cd0051e82c90b35822f641 Mon Sep 17 00:00:00 2001 From: FelixFan1992 Date: Mon, 9 Feb 2026 22:35:56 -0500 Subject: [PATCH 18/18] update --- engine/cld/config/env_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/cld/config/env_test.go b/engine/cld/config/env_test.go index ae36e32c..416db473 100644 --- a/engine/cld/config/env_test.go +++ b/engine/cld/config/env_test.go @@ -67,7 +67,7 @@ func Test_LoadEnvConfig(t *testing.T) { //nolint:paralleltest // These tests are "SOLANA_PROGRAM_PATH": "0xcde", "APTOS_DEPLOYER_KEY": "0x345", "TRON_DEPLOYER_KEY": "0x456", - "STELLAR_DEPLOYER_KEY": "0x567", + "ONCHAIN_STELLAR_DEPLOYER_KEY": "0x567", // Stellar uses new-style env var }, wantFunc: func(t *testing.T, cfg *cfgenv.Config) { t.Helper()