diff --git a/core/cmd/app.go b/core/cmd/app.go index b8d8776fbe9..d160a1a1e3b 100644 --- a/core/cmd/app.go +++ b/core/cmd/app.go @@ -206,6 +206,7 @@ func NewApp(s *Shell) *cli.App { keysCommand("Solana", NewSolanaKeysClient(s)), keysCommand("StarkNet", NewStarkNetKeysClient(s)), keysCommand("Aptos", NewAptosKeysClient(s)), + keysCommand("Stellar", NewStellarKeysClient(s)), keysCommand("Tron", NewTronKeysClient(s)), keysCommand("TON", NewTONKeysClient(s)), keysCommand("Sui", NewSuiKeysClient(s)), diff --git a/core/cmd/stellar_keys_commands.go b/core/cmd/stellar_keys_commands.go new file mode 100644 index 00000000000..44eae670441 --- /dev/null +++ b/core/cmd/stellar_keys_commands.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/stellarkey" + "github.com/smartcontractkit/chainlink-common/pkg/utils" + + "github.com/smartcontractkit/chainlink/v2/core/web/presenters" +) + +type StellarKeyPresenter struct { + JAID + presenters.StellarKeyResource +} + +// RenderTable implements TableRenderer +func (p StellarKeyPresenter) RenderTable(rt RendererTable) error { + headers := []string{"ID", "Stellar Public Key"} + rows := [][]string{p.ToRow()} + + if _, err := rt.Write([]byte("🔑 Stellar Keys\n")); err != nil { + return err + } + renderList(headers, rows, rt.Writer) + + return utils.JustError(rt.Write([]byte("\n"))) +} + +func (p *StellarKeyPresenter) ToRow() []string { + row := []string{ + p.ID, + p.PubKey, + } + + return row +} + +type StellarKeyPresenters []StellarKeyPresenter + +// RenderTable implements TableRenderer +func (ps StellarKeyPresenters) RenderTable(rt RendererTable) error { + headers := []string{"ID", "Stellar Public Key"} + rows := make([][]string, 0, len(ps)) + + for _, p := range ps { + rows = append(rows, p.ToRow()) + } + + if _, err := rt.Write([]byte("🔑 Stellar Keys\n")); err != nil { + return err + } + renderList(headers, rows, rt.Writer) + + return utils.JustError(rt.Write([]byte("\n"))) +} + +func NewStellarKeysClient(s *Shell) KeysClient { + return newKeysClient[stellarkey.Key, StellarKeyPresenter, StellarKeyPresenters]("Stellar", s) +} diff --git a/core/cmd/stellar_keys_commands_test.go b/core/cmd/stellar_keys_commands_test.go new file mode 100644 index 00000000000..52ce8f1c110 --- /dev/null +++ b/core/cmd/stellar_keys_commands_test.go @@ -0,0 +1,179 @@ +package cmd_test + +import ( + "bytes" + "context" + "flag" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/stellarkey" + "github.com/smartcontractkit/chainlink-common/pkg/utils" + + "github.com/smartcontractkit/chainlink/v2/core/cmd" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/web/presenters" +) + +func TestStellarKeyPresenter_RenderTable(t *testing.T) { + t.Parallel() + + var ( + id = "GDTERXGZ7J6NQCSTG7ZNDR7RYV6QD2K4U4XG7K6Y4GD5EQLNMXS6K4C5" + pubKey = "somepubkey" + buffer = bytes.NewBufferString("") + r = cmd.RendererTable{Writer: buffer} + ) + + p := cmd.StellarKeyPresenter{ + JAID: cmd.JAID{ID: id}, + StellarKeyResource: presenters.StellarKeyResource{ + JAID: presenters.NewJAID(id), + PubKey: pubKey, + }, + } + + // Render a single resource + require.NoError(t, p.RenderTable(r)) + + output := buffer.String() + assert.Contains(t, output, id) + assert.Contains(t, output, pubKey) + + // Render many resources + buffer.Reset() + ps := cmd.StellarKeyPresenters{p} + require.NoError(t, ps.RenderTable(r)) + + output = buffer.String() + assert.Contains(t, output, id) + assert.Contains(t, output, pubKey) +} + +//nolint:paralleltest // subtests share a single keystore/application instance +func TestShell_StellarKeys(t *testing.T) { + app := startNewApplicationV2(t, nil) + ks := app.GetKeyStore().Stellar() + cleanup := func() { + ctx := context.Background() + keys, err := ks.GetAll() + require.NoError(t, err) + for _, key := range keys { + require.NoError(t, utils.JustError(ks.Delete(ctx, key.ID()))) + } + requireStellarKeyCount(t, app, 0) + } + + //nolint:paralleltest // subtests share a single keystore/application instance + t.Run("ListStellarKeys", func(tt *testing.T) { + defer cleanup() + ctx := tt.Context() + client, r := app.NewShellAndRenderer() + key, err := app.GetKeyStore().Stellar().Create(ctx) + require.NoError(t, err) + requireStellarKeyCount(t, app, 1) + require.NoError(t, cmd.NewStellarKeysClient(client).ListKeys(cltest.EmptyCLIContext())) + require.Len(t, r.Renders, 1) + keys := *r.Renders[0].(*cmd.StellarKeyPresenters) + assert.Equal(t, key.PublicKeyStr(), keys[0].PubKey) + }) + + //nolint:paralleltest // subtests share a single keystore/application instance + t.Run("CreateStellarKey", func(tt *testing.T) { + defer cleanup() + client, _ := app.NewShellAndRenderer() + require.NoError(t, cmd.NewStellarKeysClient(client).CreateKey(nilContext)) + keys, err := app.GetKeyStore().Stellar().GetAll() + require.NoError(t, err) + require.Len(t, keys, 1) + }) + + //nolint:paralleltest // subtests share a single keystore/application instance + t.Run("DeleteStellarKey", func(tt *testing.T) { + defer cleanup() + ctx := tt.Context() + client, _ := app.NewShellAndRenderer() + key, err := app.GetKeyStore().Stellar().Create(ctx) + require.NoError(t, err) + requireStellarKeyCount(t, app, 1) + set := flag.NewFlagSet("test", 0) + flagSetApplyFromAction(cmd.NewStellarKeysClient(client).DeleteKey, set, "stellar") + + require.NoError(tt, set.Set("yes", "true")) + + strID := key.ID() + err = set.Parse([]string{strID}) + require.NoError(t, err) + c := cli.NewContext(nil, set, nil) + err = cmd.NewStellarKeysClient(client).DeleteKey(c) + require.NoError(t, err) + requireStellarKeyCount(t, app, 0) + }) + + //nolint:paralleltest // subtests share a single keystore/application instance + t.Run("ImportExportStellarKey", func(tt *testing.T) { + defer cleanup() + defer deleteKeyExportFile(t) + ctx := tt.Context() + client, _ := app.NewShellAndRenderer() + + _, err := app.GetKeyStore().Stellar().Create(ctx) + require.NoError(t, err) + + keys := requireStellarKeyCount(t, app, 1) + key := keys[0] + keyName := keyNameForTest(t) + + // Export test invalid id + set := flag.NewFlagSet("test Stellar export", 0) + flagSetApplyFromAction(cmd.NewStellarKeysClient(client).ExportKey, set, "stellar") + + require.NoError(tt, set.Parse([]string{"0"})) + require.NoError(tt, set.Set("new-password", "../internal/fixtures/incorrect_password.txt")) + require.NoError(tt, set.Set("output", keyName)) + + c := cli.NewContext(nil, set, nil) + err = cmd.NewStellarKeysClient(client).ExportKey(c) + require.Error(t, err, "Error exporting") + require.Error(t, utils.JustError(os.Stat(keyName))) + + // Export test + set = flag.NewFlagSet("test Stellar export", 0) + flagSetApplyFromAction(cmd.NewStellarKeysClient(client).ExportKey, set, "stellar") + + require.NoError(tt, set.Parse([]string{key.ID()})) + require.NoError(tt, set.Set("new-password", "../internal/fixtures/incorrect_password.txt")) + require.NoError(tt, set.Set("output", keyName)) + + c = cli.NewContext(nil, set, nil) + + require.NoError(t, cmd.NewStellarKeysClient(client).ExportKey(c)) + require.NoError(t, utils.JustError(os.Stat(keyName))) + + require.NoError(t, utils.JustError(app.GetKeyStore().Stellar().Delete(ctx, key.ID()))) + requireStellarKeyCount(t, app, 0) + + set = flag.NewFlagSet("test Stellar import", 0) + flagSetApplyFromAction(cmd.NewStellarKeysClient(client).ImportKey, set, "stellar") + + require.NoError(tt, set.Parse([]string{keyName})) + require.NoError(tt, set.Set("old-password", "../internal/fixtures/incorrect_password.txt")) + c = cli.NewContext(nil, set, nil) + require.NoError(t, cmd.NewStellarKeysClient(client).ImportKey(c)) + + requireStellarKeyCount(t, app, 1) + }) +} + +func requireStellarKeyCount(t *testing.T, app chainlink.Application, length int) []stellarkey.Key { + t.Helper() + keys, err := app.GetKeyStore().Stellar().GetAll() + require.NoError(t, err) + require.Len(t, keys, length) + return keys +} diff --git a/core/config/env/env.go b/core/config/env/env.go index 6edaeefb29a..dc5b4951fbc 100644 --- a/core/config/env/env.go +++ b/core/config/env/env.go @@ -38,6 +38,7 @@ var ( TronPlugin = NewPlugin("tron") TONPlugin = NewPlugin("ton") SuiPlugin = NewPlugin("sui") + StellarPlugin = NewPlugin("stellar") CapabilitiesPlugin = NewPlugin("capabilities") // PrometheusDiscoveryHostName is the externally accessible hostname // published by the node in the `/discovery` endpoint. Generally, it is expected to match diff --git a/core/internal/cltest/cltest.go b/core/internal/cltest/cltest.go index 4042714be6f..bb5e0b292aa 100644 --- a/core/internal/cltest/cltest.go +++ b/core/internal/cltest/cltest.go @@ -48,6 +48,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/workflows/dontime" "github.com/smartcontractkit/chainlink-data-streams/llo/retirement" "github.com/smartcontractkit/chainlink-framework/multinode" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/compute" "github.com/smartcontractkit/chainlink-evm/pkg/assets" @@ -66,12 +67,14 @@ import ( "github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/solkey" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/starkkey" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/stellarkey" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/suikey" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/tonkey" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/tronkey" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/vrfkey" "github.com/smartcontractkit/chainlink-data-streams/mercury/wsrpc" "github.com/smartcontractkit/chainlink-data-streams/mercury/wsrpc/cache" + "github.com/smartcontractkit/chainlink/v2/core/bridges" "github.com/smartcontractkit/chainlink/v2/core/capabilities" remotetypes "github.com/smartcontractkit/chainlink/v2/core/capabilities/remote/types" @@ -138,6 +141,7 @@ var ( DefaultTronKey = tronkey.MustNewInsecure(keystest.NewRandReaderFromSeed(KeyBigIntSeed)) DefaultTONKey = tonkey.MustNewInsecure(keystest.NewRandReaderFromSeed(KeyBigIntSeed)) DefaultSuiKey = suikey.MustNewInsecure(keystest.NewRandReaderFromSeed(KeyBigIntSeed)) + DefaultStellarKey = stellarkey.MustNewInsecure(keystest.NewRandReaderFromSeed(KeyBigIntSeed)) DefaultVRFKey = vrfkey.MustNewV2XXXTestingOnly(big.NewInt(KeyBigIntSeed)) ) diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 54458798f4d..3afe18bf10b 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -337,6 +337,9 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err if cfg.SuiEnabled() { initOps = append(initOps, InitSui(relayerFactory, keyStore.Sui(), keyStore.CSA(), cfg.SuiConfigs())) } + if cfg.StellarEnabled() { + initOps = append(initOps, InitStellar(relayerFactory, keyStore.Stellar(), keyStore.CSA(), cfg.StellarConfigs())) + } relayChainInterops, err := NewCoreRelayerChainInteroperators(initOps...) if err != nil { diff --git a/core/services/chainlink/node_platform.go b/core/services/chainlink/node_platform.go index a9efb861496..bad4227735d 100644 --- a/core/services/chainlink/node_platform.go +++ b/core/services/chainlink/node_platform.go @@ -249,6 +249,8 @@ func (r nodePlatformSubmitterKeyReader) submitterKeysForRelay(ctx context.Contex return nodePlatformKeyIDs(r.keyStore.TON()) case relay.NetworkSui: return nodePlatformKeyIDs(r.keyStore.Sui()) + case relay.NetworkStellar: + return nodePlatformKeyIDs(r.keyStore.Stellar()) default: return nil, nil } diff --git a/core/services/chainlink/relayer_chain_interoperators.go b/core/services/chainlink/relayer_chain_interoperators.go index ce4ea40e818..56251c39355 100644 --- a/core/services/chainlink/relayer_chain_interoperators.go +++ b/core/services/chainlink/relayer_chain_interoperators.go @@ -264,6 +264,24 @@ func InitSui(factory RelayerFactory, ks keystore.Sui, csaKS keystore.CSA, chainC } } +// InitStellar is an option for instantiating Stellar relayers. +func InitStellar(factory RelayerFactory, ks keystore.Stellar, csaKS keystore.CSA, chainCfgs RawConfigs) CoreRelayerChainInitFunc { + return func(op *CoreRelayerChainInteroperators) (err error) { + loopKs := &keystore.StellarLooppSigner{Stellar: ks} + relayers, err := factory.NewStellar(loopKs, &keystore.CSASigner{CSA: csaKS}, chainCfgs) + if err != nil { + return fmt.Errorf("failed to setup Stellar relayer: %w", err) + } + + for id, relayer := range relayers { + op.srvs = append(op.srvs, relayer) + op.loopRelayers[id] = relayer + } + + return nil + } +} + // Get a [loop.Relayer] by id func (rs *CoreRelayerChainInteroperators) Get(id types.RelayID) (loop.Relayer, error) { rs.mu.Lock() diff --git a/core/services/chainlink/relayer_chain_interoperators_test.go b/core/services/chainlink/relayer_chain_interoperators_test.go index 6d52913f107..e45b9338fa6 100644 --- a/core/services/chainlink/relayer_chain_interoperators_test.go +++ b/core/services/chainlink/relayer_chain_interoperators_test.go @@ -216,7 +216,8 @@ func TestCoreRelayerChainInteroperators(t *testing.T) { t.Skip("ton doesn't need a CoreRelayerChainInteroperator") case relay.NetworkSui: t.Skip("sui doesn't need a CoreRelayerChainInteroperator") - + case relay.NetworkStellar: + t.Skip("stellar doesn't need a CoreRelayerChainInteroperator") default: require.Fail(t, "untested relay network", relayNetwork) } diff --git a/core/services/chainlink/relayer_factory.go b/core/services/chainlink/relayer_factory.go index c8d59553ec9..7a87e0a275c 100644 --- a/core/services/chainlink/relayer_factory.go +++ b/core/services/chainlink/relayer_factory.go @@ -179,6 +179,10 @@ func (r *RelayerFactory) NewSui(ks coretypes.Keystore, ksCSA coretypes.Keystore, return r.NewLOOPRelayer("Sui", relay.NetworkSui, env.SuiPlugin, ks, ksCSA, chainCfgs) } +func (r *RelayerFactory) NewStellar(ks coretypes.Keystore, ksCSA coretypes.Keystore, chainCfgs RawConfigs) (map[types.RelayID]loop.Relayer, error) { + return r.NewLOOPRelayer("Stellar", relay.NetworkStellar, env.StellarPlugin, ks, ksCSA, chainCfgs) +} + func (r *RelayerFactory) NewLOOPRelayer(name string, network string, plugin env.Plugin, ks, ksCSA coretypes.Keystore, chainCfgs RawConfigs) (map[types.RelayID]loop.Relayer, error) { relayers := make(map[types.RelayID]loop.Relayer) lggr := logger.Named(r.Logger, name) diff --git a/core/services/job/job_orm_test.go b/core/services/job/job_orm_test.go index 84e918b78f8..236d3850e33 100644 --- a/core/services/job/job_orm_test.go +++ b/core/services/job/job_orm_test.go @@ -15,21 +15,16 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/guregu/null.v4" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ethkey" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/utils/jsonserializable" pkgworkflows "github.com/smartcontractkit/chainlink-common/pkg/workflows" - "github.com/smartcontractkit/chainlink/v2/core/services/ccv/ccvcommitteeverifier" - "github.com/smartcontractkit/chainlink/v2/core/services/ccv/ccvexecutor" - "github.com/smartcontractkit/chainlink/v2/core/services/cresettings" - "github.com/smartcontractkit/chainlink/v2/core/services/workflows/artifacts" - "github.com/smartcontractkit/chainlink-evm/pkg/assets" configtoml "github.com/smartcontractkit/chainlink-evm/pkg/config/toml" "github.com/smartcontractkit/chainlink-evm/pkg/keys" evmtypes "github.com/smartcontractkit/chainlink-evm/pkg/types" - "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ethkey" "github.com/smartcontractkit/chainlink/v2/core/bridges" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" @@ -39,7 +34,10 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/blockhashstore" "github.com/smartcontractkit/chainlink/v2/core/services/blockheaderfeeder" + "github.com/smartcontractkit/chainlink/v2/core/services/ccv/ccvcommitteeverifier" + "github.com/smartcontractkit/chainlink/v2/core/services/ccv/ccvexecutor" "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/cresettings" "github.com/smartcontractkit/chainlink/v2/core/services/cron" "github.com/smartcontractkit/chainlink/v2/core/services/gateway" "github.com/smartcontractkit/chainlink/v2/core/services/job" @@ -51,6 +49,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/standardcapabilities" "github.com/smartcontractkit/chainlink/v2/core/services/streams" "github.com/smartcontractkit/chainlink/v2/core/services/vrf/vrfcommon" + "github.com/smartcontractkit/chainlink/v2/core/services/workflows/artifacts" "github.com/smartcontractkit/chainlink/v2/core/testdata/testspecs" "github.com/smartcontractkit/chainlink/v2/core/utils/testutils/heavyweight" ) @@ -250,7 +249,6 @@ func TestORM(t *testing.T) { assert.Equal(t, ocrSpecError2, dbSpecErr2.Description) }) - t.Run("rejects webhook job creation with external initiators", func(t *testing.T) { eiFoo := cltest.MustInsertExternalInitiator(t, borm) eiBar := cltest.MustInsertExternalInitiator(t, borm) @@ -965,7 +963,7 @@ func TestORM_CreateJob_OCR2_Sending_Keys_Transmitter_Keys_Validations(t *testing } func TestORM_ValidateKeyStoreMatch(t *testing.T) { - ctx := testutils.Context(t) + ctx := testing.TB.Context(t) config := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) {}) keyStore := cltest.NewKeyStore(t, pgtest.NewSqlxDB(t)) @@ -1062,6 +1060,18 @@ func TestORM_ValidateKeyStoreMatch(t *testing.T) { require.NoError(t, err) }) + t.Run("test Stellar key validation", func(t *testing.T) { //nolint:paralleltest // same instance + ctx := testing.TB.Context(t) + jb.OCR2OracleSpec.Relay = relay.NetworkStellar + err := job.ValidateKeyStoreMatch(ctx, jb.OCR2OracleSpec, keyStore, "bad key") + require.EqualError(t, err, "no Stellar key matching: \"bad key\"") + + stellarKey, err := keyStore.Stellar().Create(ctx) + require.NoError(t, err) + err = job.ValidateKeyStoreMatch(ctx, jb.OCR2OracleSpec, keyStore, stellarKey.ID()) + require.NoError(t, err) + }) + t.Run("test Mercury ETH key validation", func(t *testing.T) { ctx := testutils.Context(t) jb.OCR2OracleSpec.PluginType = types.Mercury diff --git a/core/services/job/orm.go b/core/services/job/orm.go index f3abbd0ee8a..0f12bc18385 100644 --- a/core/services/job/orm.go +++ b/core/services/job/orm.go @@ -669,6 +669,11 @@ func validateKeyStoreMatchForRelay(ctx context.Context, network string, keyStore if err != nil { return errors.Errorf("no Sui key matching: %q", key) } + case relay.NetworkStellar: + _, err := keyStore.Stellar().Get(key) + if err != nil { + return errors.Errorf("no Stellar key matching: %q", key) + } } return nil } diff --git a/core/services/keystore/keystoretest.go b/core/services/keystore/keystoretest.go index a77824aa5bc..2b9b64d12e4 100644 --- a/core/services/keystore/keystoretest.go +++ b/core/services/keystore/keystoretest.go @@ -75,6 +75,7 @@ func NewInMemory(ds sqlutil.DataSource, scryptParams keystore.ScryptParams, logf starknet: newStarkNetKeyStore(km), sui: newSuiKeyStore(km), aptos: newAptosKeyStore(km), + stellar: newStellarKeyStore(km), tron: newTronKeyStore(km), ton: newTONKeyStore(km), vrf: newVRFKeyStore(km), diff --git a/core/services/relay/relay.go b/core/services/relay/relay.go index 2811fc050df..94babbdd426 100644 --- a/core/services/relay/relay.go +++ b/core/services/relay/relay.go @@ -17,6 +17,7 @@ const ( NetworkTron = "tron" NetworkTON = "ton" NetworkSui = "sui" + NetworkStellar = "stellar" NetworkDummy = "dummy" ) @@ -30,6 +31,7 @@ var SupportedNetworks = map[string]struct{}{ NetworkTron: {}, NetworkTON: {}, NetworkSui: {}, + NetworkStellar: {}, NetworkDummy: {}, } diff --git a/core/web/auth/auth_test.go b/core/web/auth/auth_test.go index e5d973efa95..0585ab1d57c 100644 --- a/core/web/auth/auth_test.go +++ b/core/web/auth/auth_test.go @@ -276,30 +276,35 @@ var routesRolesMap = [...]routeRules{ {"GET", "/v2/keys/cosmos", true, true, true}, {"GET", "/v2/keys/starknet", true, true, true}, {"GET", "/v2/keys/aptos", true, true, true}, + {"GET", "/v2/keys/stellar", true, true, true}, {"GET", "/v2/keys/tron", true, true, true}, {"GET", "/v2/keys/ton", true, true, true}, {"POST", "/v2/keys/solana", false, false, true}, {"POST", "/v2/keys/cosmos", false, false, true}, {"POST", "/v2/keys/starknet", false, false, true}, {"POST", "/v2/keys/aptos", false, false, true}, + {"POST", "/v2/keys/stellar", false, false, true}, {"POST", "/v2/keys/tron", false, false, true}, {"POST", "/v2/keys/ton", false, false, true}, {"DELETE", "/v2/keys/solana/MOCK", false, false, false}, {"DELETE", "/v2/keys/cosmos/MOCK", false, false, false}, {"DELETE", "/v2/keys/starknet/MOCK", false, false, false}, {"DELETE", "/v2/keys/aptos/MOCK", false, false, false}, + {"DELETE", "/v2/keys/stellar/MOCK", false, false, false}, {"DELETE", "/v2/keys/tron/MOCK", false, false, false}, {"DELETE", "/v2/keys/ton/MOCK", false, false, false}, {"POST", "/v2/keys/solana/import", false, false, false}, {"POST", "/v2/keys/cosmos/import", false, false, false}, {"POST", "/v2/keys/starknet/import", false, false, false}, {"POST", "/v2/keys/aptos/import", false, false, false}, + {"POST", "/v2/keys/stellar/import", false, false, false}, {"POST", "/v2/keys/tron/import", false, false, false}, {"POST", "/v2/keys/ton/import", false, false, false}, {"POST", "/v2/keys/solana/export/MOCK", false, false, false}, {"POST", "/v2/keys/cosmos/export/MOCK", false, false, false}, {"POST", "/v2/keys/starknet/export/MOCK", false, false, false}, {"POST", "/v2/keys/aptos/export/MOCK", false, false, false}, + {"POST", "/v2/keys/stellar/export/MOCK", false, false, false}, {"POST", "/v2/keys/tron/export/MOCK", false, false, false}, {"POST", "/v2/keys/ton/export/MOCK", false, false, false}, {"GET", "/v2/keys/vrf", true, true, true}, @@ -320,15 +325,18 @@ var routesRolesMap = [...]routeRules{ {"PATCH", "/v2/log", false, false, false}, {"GET", "/v2/chains/evm", true, true, true}, {"GET", "/v2/chains/solana", true, true, true}, + {"GET", "/v2/chains/stellar", true, true, true}, {"GET", "/v2/chains/cosmos", true, true, true}, {"GET", "/v2/chains/evm/MOCK", true, true, true}, {"GET", "/v2/chains/cosmos/MOCK", true, true, true}, {"GET", "/v2/nodes/", true, true, true}, {"GET", "/v2/nodes/evm", true, true, true}, {"GET", "/v2/nodes/solana", true, true, true}, + {"GET", "/v2/nodes/stellar", true, true, true}, {"GET", "/v2/nodes/cosmos", true, true, true}, {"GET", "/v2/chains/evm/MOCK/nodes", true, true, true}, {"GET", "/v2/chains/solana/MOCK/nodes", true, true, true}, + {"GET", "/v2/chains/stellar/MOCK/nodes", true, true, true}, {"GET", "/v2/chains/cosmos/MOCK/nodes", true, true, true}, {"GET", "/v2/nodes/evm/forwarders", true, true, true}, {"POST", "/v2/nodes/evm/forwarders/track", false, false, true}, diff --git a/core/web/presenters/stellar_key.go b/core/web/presenters/stellar_key.go new file mode 100644 index 00000000000..2d2983e9820 --- /dev/null +++ b/core/web/presenters/stellar_key.go @@ -0,0 +1,34 @@ +package presenters + +import ( + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/stellarkey" +) + +// StellarKeyResource represents a Stellar key JSONAPI resource. +type StellarKeyResource struct { + JAID + PubKey string `json:"publicKey"` +} + +// GetName implements the api2go EntityNamer interface +func (StellarKeyResource) GetName() string { + return "encryptedStellarKeys" +} + +func NewStellarKeyResource(key stellarkey.Key) *StellarKeyResource { + r := &StellarKeyResource{ + JAID: JAID{ID: key.ID()}, + PubKey: key.PublicKeyStr(), + } + + return r +} + +func NewStellarKeyResources(keys []stellarkey.Key) []StellarKeyResource { + var rs = make([]StellarKeyResource, 0, len(keys)) + for _, key := range keys { + rs = append(rs, *NewStellarKeyResource(key)) + } + + return rs +} diff --git a/core/web/router.go b/core/web/router.go index ce1160b63f7..3a93812f40c 100644 --- a/core/web/router.go +++ b/core/web/router.go @@ -360,6 +360,7 @@ func v2Routes(app chainlink.Application, r *gin.RouterGroup) { {"cosmos", NewCosmosKeysController(app)}, {"starknet", NewStarkNetKeysController(app)}, {"aptos", NewAptosKeysController(app)}, + {"stellar", NewStellarKeysController(app)}, {"tron", NewTronKeysController(app)}, {"sui", NewSuiKeysController(app)}, {"ton", NewTONKeysController(app)}, diff --git a/core/web/stellar_keys_controller.go b/core/web/stellar_keys_controller.go new file mode 100644 index 00000000000..164fd732acd --- /dev/null +++ b/core/web/stellar_keys_controller.go @@ -0,0 +1,13 @@ +package web + +import ( + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/stellarkey" + + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/web/presenters" +) + +func NewStellarKeysController(app chainlink.Application) KeysController { + return NewKeysController[stellarkey.Key, presenters.StellarKeyResource](app.GetKeyStore().Stellar(), app.GetLogger(), app.GetAuditLogger(), + "stellarKey", presenters.NewStellarKeyResource, presenters.NewStellarKeyResources) +} diff --git a/core/web/stellar_keys_controller_test.go b/core/web/stellar_keys_controller_test.go new file mode 100644 index 00000000000..0da37e416bd --- /dev/null +++ b/core/web/stellar_keys_controller_test.go @@ -0,0 +1,108 @@ +package web_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/utils" + + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore" + "github.com/smartcontractkit/chainlink/v2/core/web" + "github.com/smartcontractkit/chainlink/v2/core/web/presenters" +) + +func TestStellarKeysController_Index_HappyPath(t *testing.T) { + t.Parallel() + + client, keyStore := setupStellarKeysControllerTests(t) + keys, _ := keyStore.Stellar().GetAll() + + response, cleanup := client.Get("/v2/keys/stellar") + t.Cleanup(cleanup) + cltest.AssertServerResponse(t, response, http.StatusOK) + + var resources []presenters.StellarKeyResource + err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, response), &resources) + require.NoError(t, err) + + require.Len(t, resources, len(keys)) + + assert.Equal(t, keys[0].ID(), resources[0].ID) + assert.Equal(t, keys[0].PublicKeyStr(), resources[0].PubKey) +} + +func TestStellarKeysController_Create_HappyPath(t *testing.T) { + t.Parallel() + + app := cltest.NewApplicationEVMDisabled(t) + require.NoError(t, app.Start(testing.TB.Context(t))) + client := app.NewHTTPClient(nil) + keyStore := app.GetKeyStore() + + response, cleanup := client.Post("/v2/keys/stellar", nil) + t.Cleanup(cleanup) + cltest.AssertServerResponse(t, response, http.StatusOK) + + keys, _ := keyStore.Stellar().GetAll() + require.Len(t, keys, 1) + + resource := presenters.StellarKeyResource{} + err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, response), &resource) + require.NoError(t, err) + + assert.Equal(t, keys[0].ID(), resource.ID) + assert.Equal(t, keys[0].PublicKeyStr(), resource.PubKey) + + _, err = keyStore.Stellar().Get(resource.ID) + require.NoError(t, err) +} + +func TestStellarKeysController_Delete_NonExistentStellarKeyID(t *testing.T) { + t.Parallel() + + client, _ := setupStellarKeysControllerTests(t) + + nonExistentStellarKeyID := "foobar" + response, cleanup := client.Delete("/v2/keys/stellar/" + nonExistentStellarKeyID) + t.Cleanup(cleanup) + assert.Equal(t, http.StatusNotFound, response.StatusCode) +} + +func TestStellarKeysController_Delete_HappyPath(t *testing.T) { + t.Parallel() + ctx := testing.TB.Context(t) + + client, keyStore := setupStellarKeysControllerTests(t) + + keys, _ := keyStore.Stellar().GetAll() + initialLength := len(keys) + key, _ := keyStore.Stellar().Create(ctx) + + response, cleanup := client.Delete("/v2/keys/stellar/" + key.ID()) + t.Cleanup(cleanup) + assert.Equal(t, http.StatusOK, response.StatusCode) + require.Error(t, utils.JustError(keyStore.Stellar().Get(key.ID()))) + + keys, _ = keyStore.Stellar().GetAll() + assert.Len(t, keys, initialLength) +} + +func setupStellarKeysControllerTests(t *testing.T) (cltest.HTTPClientCleaner, keystore.Master) { + t.Helper() + ctx := testing.TB.Context(t) + + app := cltest.NewApplication(t) + require.NoError(t, app.Start(ctx)) + require.NoError(t, app.KeyStore.OCR().Add(ctx, cltest.DefaultOCRKey)) + stellarKeyStore := app.GetKeyStore().Stellar() + require.NotNil(t, stellarKeyStore) + require.NoError(t, stellarKeyStore.Add(ctx, cltest.DefaultStellarKey)) + + client := app.NewHTTPClient(nil) + + return client, app.GetKeyStore() +} diff --git a/deployment/utils/nodetestutils/node.go b/deployment/utils/nodetestutils/node.go index b4526932ff9..f9ce5e5633f 100644 --- a/deployment/utils/nodetestutils/node.go +++ b/deployment/utils/nodetestutils/node.go @@ -491,7 +491,7 @@ func NewNode( require.NoError(t, master.Unlock(ctx, "password")) require.NoError(t, master.CSA().EnsureKey(ctx)) require.NoError(t, master.Workflow().EnsureKey(ctx)) - require.NoError(t, master.OCR2().EnsureKeys(ctx, corekeys.EVM, corekeys.Solana, corekeys.Aptos)) + require.NoError(t, master.OCR2().EnsureKeys(ctx, corekeys.EVM, corekeys.Solana, corekeys.Aptos, corekeys.Stellar)) app, err := chainlink.NewApplication(ctx, chainlink.ApplicationOpts{ Opts: cre.Opts{ diff --git a/plugins/plugins.public.yaml b/plugins/plugins.public.yaml index cbe7fe86e43..d272bb92c64 100644 --- a/plugins/plugins.public.yaml +++ b/plugins/plugins.public.yaml @@ -43,6 +43,11 @@ plugins: gitRef: "5d83a289da752e966692a9f390970dbc86c95c7a" # chainlink-starknet develop (#695 merged) installPath: "./pkg/chainlink/cmd/chainlink-starknet" + stellar: + - moduleURI: "github.com/smartcontractkit/chainlink-stellar" + gitRef: "0e9836f8575b711d87335fbadb1ed28b8f678377" # chainlink-stellar #128 (add-multinode merged) + installPath: "./cmd/chainlink-stellar" + streams: - moduleURI: "github.com/smartcontractkit/chainlink-data-streams" gitRef: "v0.1.15-0.20260522094612-5f9f748bd87a" diff --git a/testdata/scripts/chains/help.txtar b/testdata/scripts/chains/help.txtar index 5aedccd4281..fa36a19a67c 100644 --- a/testdata/scripts/chains/help.txtar +++ b/testdata/scripts/chains/help.txtar @@ -14,6 +14,7 @@ COMMANDS: evm Commands for handling evm chains solana Commands for handling solana chains starknet Commands for handling starknet chains + stellar Commands for handling stellar chains sui Commands for handling sui chains ton Commands for handling ton chains tron Commands for handling tron chains diff --git a/testdata/scripts/chains/stellar/help.txtar b/testdata/scripts/chains/stellar/help.txtar new file mode 100644 index 00000000000..686fe4435a9 --- /dev/null +++ b/testdata/scripts/chains/stellar/help.txtar @@ -0,0 +1,16 @@ +exec chainlink chains stellar --help +cmp stdout out.txt + +-- out.txt -- +NAME: + chainlink chains stellar - Commands for handling stellar chains + +USAGE: + chainlink chains stellar command [command options] [arguments...] + +COMMANDS: + list List all existing stellar chains + +OPTIONS: + --help, -h show help + diff --git a/testdata/scripts/chains/stellar/list/help.txtar b/testdata/scripts/chains/stellar/list/help.txtar new file mode 100644 index 00000000000..c64736f9f2d --- /dev/null +++ b/testdata/scripts/chains/stellar/list/help.txtar @@ -0,0 +1,9 @@ +exec chainlink chains stellar list --help +cmp stdout out.txt + +-- out.txt -- +NAME: + chainlink chains stellar list - List all existing stellar chains + +USAGE: + chainlink chains stellar list [arguments...] diff --git a/testdata/scripts/health/multi-chain-loopp.txtar b/testdata/scripts/health/multi-chain-loopp.txtar index c265928b150..c80bde92c6f 100644 --- a/testdata/scripts/health/multi-chain-loopp.txtar +++ b/testdata/scripts/health/multi-chain-loopp.txtar @@ -102,6 +102,13 @@ Name = 'example' URL = 'http://tron.org' SolidityURL = 'https://solidity.evm' +[[Stellar]] +ChainID = 'cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472' + +[[Stellar.Nodes]] +Name = 'primary' +URL = 'http://stellar.rpc' + -- out.txt -- ok Aptos.4.RelayerService ok Aptos.4.RelayerService.PluginRelayerClient @@ -164,6 +171,14 @@ ok StarkNet.Baz.RelayerService.PluginRelayerClient.PluginStarknet.Chain ok StarkNet.Baz.RelayerService.PluginRelayerClient.PluginStarknet.Chain.Txm ok StarkNet.Baz.RelayerService.PluginRelayerClient.PluginStarknet.PluginRelayerConfigEmitter ok StarkNet.Baz.RelayerService.PluginRelayerClient.PluginStarknet.Relayer +ok Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService +ok Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient +ok Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar +ok Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.PluginRelayerConfigEmitter +ok Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarChain +ok Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarChain.MultiNode +ok Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarChain.StellarTxm +ok Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarRelayer ok Sui.67.RelayerService ok Sui.67.RelayerService.PluginRelayerClient ok Sui.67.RelayerService.PluginRelayerClient.PluginSui @@ -735,6 +750,78 @@ ok WorkflowStore "output": "" } }, + { + "type": "checks", + "id": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService", + "attributes": { + "name": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService", + "status": "passing", + "output": "" + } + }, + { + "type": "checks", + "id": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient", + "attributes": { + "name": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient", + "status": "passing", + "output": "" + } + }, + { + "type": "checks", + "id": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar", + "attributes": { + "name": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar", + "status": "passing", + "output": "" + } + }, + { + "type": "checks", + "id": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.PluginRelayerConfigEmitter", + "attributes": { + "name": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.PluginRelayerConfigEmitter", + "status": "passing", + "output": "" + } + }, + { + "type": "checks", + "id": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarChain", + "attributes": { + "name": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarChain", + "status": "passing", + "output": "" + } + }, + { + "type": "checks", + "id": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarChain.MultiNode", + "attributes": { + "name": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarChain.MultiNode", + "status": "passing", + "output": "" + } + }, + { + "type": "checks", + "id": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarChain.StellarTxm", + "attributes": { + "name": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarChain.StellarTxm", + "status": "passing", + "output": "" + } + }, + { + "type": "checks", + "id": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarRelayer", + "attributes": { + "name": "Stellar.cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472.RelayerService.PluginRelayerClient.PluginStellar.StellarRelayer", + "status": "passing", + "output": "" + } + }, { "type": "checks", "id": "Sui.67.RelayerService", diff --git a/testdata/scripts/help-all/help-all.txtar b/testdata/scripts/help-all/help-all.txtar index 6e356c6db2d..99b03516abc 100644 --- a/testdata/scripts/help-all/help-all.txtar +++ b/testdata/scripts/help-all/help-all.txtar @@ -34,6 +34,8 @@ chains solana # Commands for handling solana chains chains solana list # List all existing solana chains chains starknet # Commands for handling starknet chains chains starknet list # List all existing starknet chains +chains stellar # Commands for handling stellar chains +chains stellar list # List all existing stellar chains chains sui # Commands for handling sui chains chains sui list # List all existing sui chains chains ton # Commands for handling ton chains @@ -117,6 +119,12 @@ keys starknet delete # Delete StarkNet key if present keys starknet export # Export StarkNet key to keyfile keys starknet import # Import StarkNet key from keyfile keys starknet list # List the StarkNet keys +keys stellar # Remote commands for administering the node's Stellar keys +keys stellar create # Create a Stellar key +keys stellar delete # Delete Stellar key if present +keys stellar export # Export Stellar key to keyfile +keys stellar import # Import Stellar key from keyfile +keys stellar list # List the Stellar keys keys sui # Remote commands for administering the node's Sui keys keys sui create # Create a Sui key keys sui delete # Delete Sui key if present @@ -174,6 +182,8 @@ nodes solana # Commands for handling solana node configuration nodes solana list # List all existing solana nodes nodes starknet # Commands for handling starknet node configuration nodes starknet list # List all existing starknet nodes +nodes stellar # Commands for handling stellar node configuration +nodes stellar list # List all existing stellar nodes nodes sui # Commands for handling sui node configuration nodes sui list # List all existing sui nodes nodes ton # Commands for handling ton node configuration diff --git a/testdata/scripts/keys/help.txtar b/testdata/scripts/keys/help.txtar index f6df3bc6090..38e3dfa4d3a 100644 --- a/testdata/scripts/keys/help.txtar +++ b/testdata/scripts/keys/help.txtar @@ -18,6 +18,7 @@ COMMANDS: solana Remote commands for administering the node's Solana keys starknet Remote commands for administering the node's StarkNet keys aptos Remote commands for administering the node's Aptos keys + stellar Remote commands for administering the node's Stellar keys tron Remote commands for administering the node's Tron keys ton Remote commands for administering the node's TON keys sui Remote commands for administering the node's Sui keys diff --git a/testdata/scripts/keys/stellar/help.txtar b/testdata/scripts/keys/stellar/help.txtar new file mode 100644 index 00000000000..93af2b9d612 --- /dev/null +++ b/testdata/scripts/keys/stellar/help.txtar @@ -0,0 +1,20 @@ +exec chainlink keys stellar --help +cmp stdout out.txt + +-- out.txt -- +NAME: + chainlink keys stellar - Remote commands for administering the node's Stellar keys + +USAGE: + chainlink keys stellar command [command options] [arguments...] + +COMMANDS: + create Create a Stellar key + import Import Stellar key from keyfile + export Export Stellar key to keyfile + delete Delete Stellar key if present + list List the Stellar keys + +OPTIONS: + --help, -h show help + diff --git a/testdata/scripts/nodes/help.txtar b/testdata/scripts/nodes/help.txtar index 67511bd4f75..c6ffcbdd7c9 100644 --- a/testdata/scripts/nodes/help.txtar +++ b/testdata/scripts/nodes/help.txtar @@ -14,6 +14,7 @@ COMMANDS: evm Commands for handling evm node configuration solana Commands for handling solana node configuration starknet Commands for handling starknet node configuration + stellar Commands for handling stellar node configuration sui Commands for handling sui node configuration ton Commands for handling ton node configuration tron Commands for handling tron node configuration diff --git a/testdata/scripts/nodes/stellar/help.txtar b/testdata/scripts/nodes/stellar/help.txtar new file mode 100644 index 00000000000..9f6a3c6f4c7 --- /dev/null +++ b/testdata/scripts/nodes/stellar/help.txtar @@ -0,0 +1,16 @@ +exec chainlink nodes stellar --help +cmp stdout out.txt + +-- out.txt -- +NAME: + chainlink nodes stellar - Commands for handling stellar node configuration + +USAGE: + chainlink nodes stellar command [command options] [arguments...] + +COMMANDS: + list List all existing stellar nodes + +OPTIONS: + --help, -h show help + diff --git a/testdata/scripts/nodes/stellar/list/help.txtar b/testdata/scripts/nodes/stellar/list/help.txtar new file mode 100644 index 00000000000..9349cf46a22 --- /dev/null +++ b/testdata/scripts/nodes/stellar/list/help.txtar @@ -0,0 +1,9 @@ +exec chainlink nodes stellar list --help +cmp stdout out.txt + +-- out.txt -- +NAME: + chainlink nodes stellar list - List all existing stellar nodes + +USAGE: + chainlink nodes stellar list [arguments...]