diff --git a/core/cmd/cosmos_node_commands_test.go b/core/cmd/cosmos_node_commands_test.go index 728be9396f9..3197c48aa94 100644 --- a/core/cmd/cosmos_node_commands_test.go +++ b/core/cmd/cosmos_node_commands_test.go @@ -48,8 +48,8 @@ func TestShell_IndexCosmosNodes(t *testing.T) { nodes := *r.Renders[0].(*cmd.CosmosNodePresenters) require.Len(t, nodes, 1) n := nodes[0] + assert.Equal(t, cltest.FormatWithPrefixedChainID(chainID, *node.Name), n.ID) assert.Equal(t, chainID, n.ChainID) - assert.Equal(t, *node.Name, n.ID) assert.Equal(t, *node.Name, n.Name) wantConfig, err := toml.Marshal(node) require.NoError(t, err) diff --git a/core/cmd/evm_node_commands_test.go b/core/cmd/evm_node_commands_test.go index dae950fce01..96269c9e028 100644 --- a/core/cmd/evm_node_commands_test.go +++ b/core/cmd/evm_node_commands_test.go @@ -60,13 +60,13 @@ func TestShell_IndexEVMNodes(t *testing.T) { n1 := nodes[0] n2 := nodes[1] assert.Equal(t, chainID.String(), n1.ChainID) - assert.Equal(t, *node1.Name, n1.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(chainID.String(), *node1.Name), n1.ID) assert.Equal(t, *node1.Name, n1.Name) wantConfig, err := toml.Marshal(node1) require.NoError(t, err) assert.Equal(t, string(wantConfig), n1.Config) assert.Equal(t, chainID.String(), n2.ChainID) - assert.Equal(t, *node2.Name, n2.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(chainID.String(), *node2.Name), n2.ID) assert.Equal(t, *node2.Name, n2.Name) wantConfig2, err := toml.Marshal(node2) require.NoError(t, err) diff --git a/core/cmd/ocr2_keys_commands_test.go b/core/cmd/ocr2_keys_commands_test.go index 5a861fafa7c..eff44685612 100644 --- a/core/cmd/ocr2_keys_commands_test.go +++ b/core/cmd/ocr2_keys_commands_test.go @@ -32,7 +32,7 @@ func TestOCR2KeyBundlePresenter_RenderTable(t *testing.T) { pubKeyConfig := key.ConfigEncryptionPublicKey() pubKey := key.OffchainPublicKey() p := cmd.OCR2KeyBundlePresenter{ - JAID: cmd.JAID{ID: bundleID}, + JAID: cmd.NewJAID(bundleID), OCR2KeysBundleResource: presenters.OCR2KeysBundleResource{ JAID: presenters.NewJAID(key.ID()), ChainType: "evm", diff --git a/core/cmd/solana_node_commands_test.go b/core/cmd/solana_node_commands_test.go index 316cf16212d..ebe9502d1fa 100644 --- a/core/cmd/solana_node_commands_test.go +++ b/core/cmd/solana_node_commands_test.go @@ -55,13 +55,13 @@ func TestShell_IndexSolanaNodes(t *testing.T) { n1 := nodes[0] n2 := nodes[1] assert.Equal(t, id, n1.ChainID) - assert.Equal(t, *node1.Name, n1.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(id, *node1.Name), n1.ID) assert.Equal(t, *node1.Name, n1.Name) wantConfig, err := toml.Marshal(node1) require.NoError(t, err) assert.Equal(t, string(wantConfig), n1.Config) assert.Equal(t, id, n2.ChainID) - assert.Equal(t, *node2.Name, n2.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(id, *node2.Name), n2.ID) assert.Equal(t, *node2.Name, n2.Name) wantConfig2, err := toml.Marshal(node2) require.NoError(t, err) diff --git a/core/cmd/starknet_node_commands_test.go b/core/cmd/starknet_node_commands_test.go index 0347cdd18f7..95f712d29bd 100644 --- a/core/cmd/starknet_node_commands_test.go +++ b/core/cmd/starknet_node_commands_test.go @@ -54,13 +54,13 @@ func TestShell_IndexStarkNetNodes(t *testing.T) { n1 := nodes[0] n2 := nodes[1] assert.Equal(t, id, n1.ChainID) - assert.Equal(t, *node1.Name, n1.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(id, *node1.Name), n1.ID) assert.Equal(t, *node1.Name, n1.Name) wantConfig, err := toml.Marshal(node1) require.NoError(t, err) assert.Equal(t, string(wantConfig), n1.Config) assert.Equal(t, id, n2.ChainID) - assert.Equal(t, *node2.Name, n2.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(id, *node2.Name), n2.ID) assert.Equal(t, *node2.Name, n2.Name) wantConfig2, err := toml.Marshal(node2) require.NoError(t, err) diff --git a/core/internal/cltest/cltest.go b/core/internal/cltest/cltest.go index c7abfb31a2a..332513b28d4 100644 --- a/core/internal/cltest/cltest.go +++ b/core/internal/cltest/cltest.go @@ -165,6 +165,10 @@ func MustRandomBytes(t *testing.T, l int) (b []byte) { return b } +func FormatWithPrefixedChainID(chainID, id string) string { + return fmt.Sprintf("%s/%s", chainID, id) +} + type JobPipelineV2TestHelper struct { Prm pipeline.ORM Jrm job.ORM diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index a9f9c22df52..e7f867c54fb 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -22,6 +22,7 @@ import ( commonservices "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/mailbox" + "github.com/smartcontractkit/chainlink/v2/core/capabilities" "github.com/smartcontractkit/chainlink/v2/core/static" "github.com/smartcontractkit/chainlink/v2/core/bridges" @@ -57,6 +58,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/telemetry" "github.com/smartcontractkit/chainlink/v2/core/services/vrf" "github.com/smartcontractkit/chainlink/v2/core/services/webhook" + "github.com/smartcontractkit/chainlink/v2/core/services/workflows" "github.com/smartcontractkit/chainlink/v2/core/sessions" "github.com/smartcontractkit/chainlink/v2/core/sessions/ldapauth" "github.com/smartcontractkit/chainlink/v2/core/sessions/localauth" @@ -183,6 +185,7 @@ func NewApplication(opts ApplicationOpts) (Application, error) { keyStore := opts.KeyStore restrictedHTTPClient := opts.RestrictedHTTPClient unrestrictedHTTPClient := opts.UnrestrictedHTTPClient + registry := capabilities.NewRegistry() // LOOPs can be created as options, in the case of LOOP relayers, or // as OCR2 job implementations, in the case of Median today. @@ -351,6 +354,10 @@ func NewApplication(opts ApplicationOpts) (Application, error) { streamRegistry, pipelineRunner, cfg.JobPipeline()), + job.Workflow: workflows.NewDelegate( + globalLogger, + registry, + ), } webhookJobRunner = delegates[job.Webhook].(*webhook.Delegate).WebhookJobRunner() ) diff --git a/core/services/job/models.go b/core/services/job/models.go index 2aee9182a9c..b769106d647 100644 --- a/core/services/job/models.go +++ b/core/services/job/models.go @@ -48,6 +48,7 @@ const ( Stream Type = (Type)(pipeline.StreamJobType) VRF Type = (Type)(pipeline.VRFJobType) Webhook Type = (Type)(pipeline.WebhookJobType) + Workflow Type = (Type)(pipeline.WorkflowJobType) ) //revive:disable:redefines-builtin-id diff --git a/core/services/job/orm.go b/core/services/job/orm.go index 475b749dfc5..07b9cb95aae 100644 --- a/core/services/job/orm.go +++ b/core/services/job/orm.go @@ -442,6 +442,8 @@ func (o *orm) CreateJob(jb *Job, qopts ...pg.QOpt) error { jb.GatewaySpecID = &specID case Stream: // 'stream' type has no associated spec, nothing to do here + case Workflow: + // 'workflow' type has no associated spec, nothing to do here default: o.lggr.Panicf("Unsupported jb.Type: %v", jb.Type) } diff --git a/core/services/pipeline/common.go b/core/services/pipeline/common.go index 74e13ae200c..1c8703e2eb1 100644 --- a/core/services/pipeline/common.go +++ b/core/services/pipeline/common.go @@ -44,6 +44,7 @@ const ( StreamJobType string = "stream" VRFJobType string = "vrf" WebhookJobType string = "webhook" + WorkflowJobType string = "workflow" ) //go:generate mockery --quiet --name Config --output ./mocks/ --case=underscore diff --git a/core/services/workflows/delegate.go b/core/services/workflows/delegate.go new file mode 100644 index 00000000000..1e48e229da5 --- /dev/null +++ b/core/services/workflows/delegate.go @@ -0,0 +1,40 @@ +package workflows + +import ( + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" + "github.com/smartcontractkit/chainlink/v2/core/services/pg" +) + +type Delegate struct { + registry types.CapabilitiesRegistry + logger logger.Logger +} + +var _ job.Delegate = (*Delegate)(nil) + +func (d *Delegate) JobType() job.Type { + return job.Workflow +} + +func (d *Delegate) BeforeJobCreated(spec job.Job) {} + +func (d *Delegate) AfterJobCreated(jb job.Job) {} + +func (d *Delegate) BeforeJobDeleted(spec job.Job) {} + +func (d *Delegate) OnDeleteJob(jb job.Job, q pg.Queryer) error { return nil } + +// ServicesForSpec satisfies the job.Delegate interface. +func (d *Delegate) ServicesForSpec(spec job.Job) ([]job.ServiceCtx, error) { + engine, err := NewEngine(d.logger, d.registry) + if err != nil { + return nil, err + } + return []job.ServiceCtx{engine}, nil +} + +func NewDelegate(logger logger.Logger, registry types.CapabilitiesRegistry) *Delegate { + return &Delegate{logger: logger, registry: registry} +} diff --git a/core/services/workflows/engine.go b/core/services/workflows/engine.go new file mode 100644 index 00000000000..1f34b58105d --- /dev/null +++ b/core/services/workflows/engine.go @@ -0,0 +1,216 @@ +package workflows + +import ( + "context" + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +const ( + mockedWorkflowID = "ef7c8168-f4d1-422f-a4b2-8ce0a1075f0a" + mockedTriggerID = "bd727a82-5cac-4071-be62-0152dd9adb0f" +) + +type Engine struct { + services.StateMachine + logger logger.Logger + registry types.CapabilitiesRegistry + trigger capabilities.TriggerCapability + consensus capabilities.ConsensusCapability + target capabilities.TargetCapability + callbackCh chan capabilities.CapabilityResponse + cancel func() +} + +func (e *Engine) Start(ctx context.Context) error { + return e.StartOnce("Engine", func() error { + err := e.registerTrigger(ctx) + if err != nil { + return err + } + + // create a new context, since the one passed in via Start is short-lived. + ctx, cancel := context.WithCancel(context.Background()) + e.cancel = cancel + go e.loop(ctx) + return nil + }) +} + +func (e *Engine) registerTrigger(ctx context.Context) error { + triggerConf, err := values.NewMap( + map[string]any{ + "feedlist": []any{ + // ETHUSD, LINKUSD, USDBTC + 123, 456, 789, + }, + }, + ) + if err != nil { + return err + } + + triggerInputs, err := values.NewMap( + map[string]any{ + "triggerId": mockedTriggerID, + }, + ) + if err != nil { + return err + } + + triggerRegRequest := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: mockedWorkflowID, + }, + Config: triggerConf, + Inputs: triggerInputs, + } + err = e.trigger.RegisterTrigger(ctx, e.callbackCh, triggerRegRequest) + if err != nil { + return fmt.Errorf("failed to instantiate mercury_trigger, %s", err) + } + return nil +} + +func (e *Engine) loop(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case resp := <-e.callbackCh: + err := e.handleExecution(ctx, resp) + if err != nil { + e.logger.Error("error executing event %+v: %w", resp, err) + } + } + } +} + +func (e *Engine) handleExecution(ctx context.Context, resp capabilities.CapabilityResponse) error { + results, err := e.handleConsensus(ctx, resp) + if err != nil { + return err + } + + _, err = e.handleTarget(ctx, results) + return err +} + +func (e *Engine) handleTarget(ctx context.Context, resp *values.List) (*values.List, error) { + report, err := resp.Unwrap() + if err != nil { + return nil, err + } + inputs := map[string]values.Value{ + "report": resp, + } + config, err := values.NewMap(map[string]any{ + "address": "0xaabbcc", + "method": "updateFeedValues(report bytes, role uint8)", + "params": []any{ + report, 1, + }, + }) + if err != nil { + return nil, err + } + + tr := capabilities.CapabilityRequest{ + Inputs: &values.Map{Underlying: inputs}, + Config: config, + Metadata: capabilities.RequestMetadata{ + WorkflowID: mockedWorkflowID, + }, + } + return capabilities.ExecuteSync(ctx, e.target, tr) +} + +func (e *Engine) handleConsensus(ctx context.Context, resp capabilities.CapabilityResponse) (*values.List, error) { + inputs := map[string]values.Value{ + "observations": resp.Value, + } + config, err := values.NewMap(map[string]any{ + "aggregation_method": "data_feeds_2_0", + "aggregation_config": map[string]any{ + // ETHUSD + "123": map[string]any{ + "deviation": "0.005", + "heartbeat": "24h", + }, + // LINKUSD + "456": map[string]any{ + "deviation": "0.001", + "heartbeat": "24h", + }, + // BTCUSD + "789": map[string]any{ + "deviation": "0.002", + "heartbeat": "6h", + }, + }, + "encoder": "EVM", + }) + if err != nil { + return nil, nil + } + cr := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: mockedWorkflowID, + }, + Inputs: &values.Map{Underlying: inputs}, + Config: config, + } + return capabilities.ExecuteSync(ctx, e.consensus, cr) +} + +func (e *Engine) Close() error { + return e.StopOnce("Engine", func() error { + defer e.cancel() + + triggerInputs, err := values.NewMap( + map[string]any{ + "triggerId": mockedTriggerID, + }, + ) + if err != nil { + return err + } + deregRequest := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: mockedWorkflowID, + }, + Inputs: triggerInputs, + } + return e.trigger.UnregisterTrigger(context.Background(), deregRequest) + }) +} + +func NewEngine(lggr logger.Logger, registry types.CapabilitiesRegistry) (*Engine, error) { + ctx := context.Background() + trigger, err := registry.GetTrigger(ctx, "on_mercury_report") + if err != nil { + return nil, err + } + consensus, err := registry.GetConsensus(ctx, "off-chain-reporting") + if err != nil { + return nil, err + } + target, err := registry.GetTarget(ctx, "write_polygon_mainnet") + if err != nil { + return nil, err + } + return &Engine{ + logger: lggr, + registry: registry, + trigger: trigger, + consensus: consensus, + target: target, + callbackCh: make(chan capabilities.CapabilityResponse), + }, nil +} diff --git a/core/services/workflows/engine_test.go b/core/services/workflows/engine_test.go new file mode 100644 index 00000000000..603f5eee3b1 --- /dev/null +++ b/core/services/workflows/engine_test.go @@ -0,0 +1,125 @@ +package workflows + +import ( + "context" + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + "github.com/smartcontractkit/chainlink-common/pkg/values" + coreCap "github.com/smartcontractkit/chainlink/v2/core/capabilities" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +type mockCapability struct { + capabilities.CapabilityInfo + capabilities.CallbackExecutable + response chan capabilities.CapabilityResponse + transform func(capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) +} + +func newMockCapability(info capabilities.CapabilityInfo, transform func(capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error)) *mockCapability { + return &mockCapability{ + transform: transform, + CapabilityInfo: info, + response: make(chan capabilities.CapabilityResponse, 10), + } +} + +func (m *mockCapability) Execute(ctx context.Context, ch chan<- capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { + cr, err := m.transform(req) + if err != nil { + return err + } + + ch <- cr + close(ch) + m.response <- cr + return nil +} + +type mockTriggerCapability struct { + capabilities.CapabilityInfo + ch chan<- capabilities.CapabilityResponse +} + +var _ capabilities.TriggerCapability = (*mockTriggerCapability)(nil) + +func (m *mockTriggerCapability) RegisterTrigger(ctx context.Context, ch chan<- capabilities.CapabilityResponse, req capabilities.CapabilityRequest) error { + m.ch = ch + return nil +} + +func (m *mockTriggerCapability) UnregisterTrigger(ctx context.Context, req capabilities.CapabilityRequest) error { + return nil +} + +func TestEngineWithHardcodedWorkflow(t *testing.T) { + ctx := context.Background() + reg := coreCap.NewRegistry() + + trigger := &mockTriggerCapability{ + CapabilityInfo: capabilities.MustNewCapabilityInfo( + "on_mercury_report", + capabilities.CapabilityTypeTrigger, + "issues a trigger when a mercury report is received.", + "v1.0.0", + ), + } + require.NoError(t, reg.Add(ctx, trigger)) + + consensus := newMockCapability( + capabilities.MustNewCapabilityInfo( + "off-chain-reporting", + capabilities.CapabilityTypeConsensus, + "an ocr3 consensus capability", + "v3.0.0", + ), + func(req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + return capabilities.CapabilityResponse{ + Value: req.Inputs.Underlying["observations"], + }, nil + }, + ) + require.NoError(t, reg.Add(ctx, consensus)) + + target := newMockCapability( + capabilities.MustNewCapabilityInfo( + "write_polygon_mainnet", + capabilities.CapabilityTypeTarget, + "a write capability targeting polygon mainnet", + "v1.0.0", + ), + func(req capabilities.CapabilityRequest) (capabilities.CapabilityResponse, error) { + + list := req.Inputs.Underlying["report"].(*values.List) + return capabilities.CapabilityResponse{ + Value: list.Underlying[0], + }, nil + }, + ) + require.NoError(t, reg.Add(ctx, target)) + + lggr := logger.TestLogger(t) + eng, err := NewEngine(lggr, reg) + require.NoError(t, err) + + err = eng.Start(ctx) + require.NoError(t, err) + defer eng.Close() + + resp, err := values.NewMap(map[string]any{ + "123": decimal.NewFromFloat(1.00), + "456": decimal.NewFromFloat(1.25), + "789": decimal.NewFromFloat(1.50), + }) + require.NoError(t, err) + cr := capabilities.CapabilityResponse{ + Value: resp, + } + trigger.ch <- cr + assert.Equal(t, cr, <-target.response) +} diff --git a/core/web/eth_keys_controller.go b/core/web/eth_keys_controller.go index fe76e8863ef..4e95bc3cb89 100644 --- a/core/web/eth_keys_controller.go +++ b/core/web/eth_keys_controller.go @@ -270,7 +270,7 @@ func (ekc *ETHKeysController) Chain(c *gin.Context) { jsonAPIError(c, http.StatusBadRequest, errors.Errorf("invalid address: %s, must be hex address", keyID)) return } - address := common.HexToAddress((keyID)) + address := common.HexToAddress(keyID) cid := c.Query("evmChainID") chain, ok := ekc.getChain(c, cid) diff --git a/core/web/eth_keys_controller_test.go b/core/web/eth_keys_controller_test.go index a9be5517bcc..e075b3196e1 100644 --- a/core/web/eth_keys_controller_test.go +++ b/core/web/eth_keys_controller_test.go @@ -284,7 +284,8 @@ func TestETHKeysController_ChainSuccess_UpdateNonce(t *testing.T) { err := cltest.ParseJSONAPIResponse(t, resp, &updatedKey) assert.NoError(t, err) - assert.Equal(t, key.ID(), updatedKey.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(cltest.FixtureChainID.String(), key.Address.String()), updatedKey.ID) + assert.Equal(t, key.Address.String(), updatedKey.Address) assert.Equal(t, cltest.FixtureChainID.String(), updatedKey.EVMChainID.String()) assert.Equal(t, false, updatedKey.Disabled) } @@ -328,7 +329,8 @@ func TestETHKeysController_ChainSuccess_Disable(t *testing.T) { err := cltest.ParseJSONAPIResponse(t, resp, &updatedKey) assert.NoError(t, err) - assert.Equal(t, key.ID(), updatedKey.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(updatedKey.EVMChainID.String(), key.Address.String()), updatedKey.ID) + assert.Equal(t, key.Address.String(), updatedKey.Address) assert.Equal(t, cltest.FixtureChainID.String(), updatedKey.EVMChainID.String()) assert.Equal(t, true, updatedKey.Disabled) } @@ -371,7 +373,8 @@ func TestETHKeysController_ChainSuccess_Enable(t *testing.T) { err := cltest.ParseJSONAPIResponse(t, resp, &updatedKey) assert.NoError(t, err) - assert.Equal(t, key.ID(), updatedKey.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(cltest.FixtureChainID.String(), key.Address.String()), updatedKey.ID) + assert.Equal(t, key.Address.String(), updatedKey.Address) assert.Equal(t, cltest.FixtureChainID.String(), updatedKey.EVMChainID.String()) assert.Equal(t, false, updatedKey.Disabled) } @@ -436,7 +439,8 @@ func TestETHKeysController_ChainSuccess_ResetWithAbandon(t *testing.T) { err = cltest.ParseJSONAPIResponse(t, resp, &updatedKey) assert.NoError(t, err) - assert.Equal(t, key.ID(), updatedKey.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(cltest.FixtureChainID.String(), key.Address.String()), updatedKey.ID) + assert.Equal(t, key.Address.String(), updatedKey.Address) assert.Equal(t, cltest.FixtureChainID.String(), updatedKey.EVMChainID.String()) assert.Equal(t, false, updatedKey.Disabled) @@ -663,7 +667,8 @@ func TestETHKeysController_DeleteSuccess(t *testing.T) { err := cltest.ParseJSONAPIResponse(t, resp, &deletedKey) assert.NoError(t, err) - assert.Equal(t, key0.ID(), deletedKey.ID) + assert.Equal(t, cltest.FormatWithPrefixedChainID(cltest.FixtureChainID.String(), key0.Address.String()), deletedKey.ID) + assert.Equal(t, key0.Address.String(), deletedKey.Address) assert.Equal(t, cltest.FixtureChainID.String(), deletedKey.EVMChainID.String()) assert.Equal(t, false, deletedKey.Disabled) diff --git a/core/web/presenters/chain_msg_test.go b/core/web/presenters/chain_msg_test.go new file mode 100644 index 00000000000..58192caef71 --- /dev/null +++ b/core/web/presenters/chain_msg_test.go @@ -0,0 +1,69 @@ +package presenters + +import ( + "fmt" + "testing" + + "github.com/manyminds/api2go/jsonapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/cosmostest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/solanatest" +) + +func TestSolanaMessageResource(t *testing.T) { + id := "1" + chainID := solanatest.RandomChainID() + r := NewSolanaMsgResource(id, chainID) + assert.Equal(t, chainID, r.ChainID) + + b, err := jsonapi.Marshal(r) + require.NoError(t, err) + + expected := fmt.Sprintf(` + { + "data":{ + "type":"solana_messages", + "id":"%s/%s", + "attributes":{ + "ChainID":"%s", + "from":"", + "to":"", + "amount":0 + } + } + } + `, chainID, id, chainID) + + assert.JSONEq(t, expected, string(b)) +} + +func TestCosmosMessageResource(t *testing.T) { + id := "1" + chainID := cosmostest.RandomChainID() + contractID := "cosmos1p3ucd3ptpw902fluyjzkq3fflq4btddac9sa3s" + r := NewCosmosMsgResource(id, chainID, contractID) + assert.Equal(t, chainID, r.ChainID) + assert.Equal(t, contractID, r.ContractID) + + b, err := jsonapi.Marshal(r) + require.NoError(t, err) + + expected := fmt.Sprintf(` + { + "data":{ + "type":"cosmos_messages", + "id":"%s/%s", + "attributes":{ + "ChainID":"%s", + "ContractID":"%s", + "State":"", + "TxHash":null + } + } + } + `, chainID, id, chainID, contractID) + + assert.JSONEq(t, expected, string(b)) +} diff --git a/core/web/presenters/cosmos_chain.go b/core/web/presenters/cosmos_chain.go index c3b006e5c7e..c2bc4b52b61 100644 --- a/core/web/presenters/cosmos_chain.go +++ b/core/web/presenters/cosmos_chain.go @@ -36,7 +36,7 @@ func (r CosmosNodeResource) GetName() string { // NewCosmosNodeResource returns a new CosmosNodeResource for node. func NewCosmosNodeResource(node types.NodeStatus) CosmosNodeResource { return CosmosNodeResource{NodeResource{ - JAID: NewJAID(node.Name), + JAID: NewPrefixedJAID(node.Name, node.ChainID), ChainID: node.ChainID, Name: node.Name, State: node.State, diff --git a/core/web/presenters/cosmos_msg.go b/core/web/presenters/cosmos_msg.go index 5bf0bb9b4f8..ab43d394ede 100644 --- a/core/web/presenters/cosmos_msg.go +++ b/core/web/presenters/cosmos_msg.go @@ -17,7 +17,7 @@ func (CosmosMsgResource) GetName() string { // NewCosmosMsgResource returns a new partial CosmosMsgResource. func NewCosmosMsgResource(id string, chainID string, contractID string) CosmosMsgResource { return CosmosMsgResource{ - JAID: NewJAID(id), + JAID: NewPrefixedJAID(id, chainID), ChainID: chainID, ContractID: contractID, } diff --git a/core/web/presenters/eth_key.go b/core/web/presenters/eth_key.go index d661d4334cd..812adeb13fa 100644 --- a/core/web/presenters/eth_key.go +++ b/core/web/presenters/eth_key.go @@ -40,7 +40,7 @@ type NewETHKeyOption func(*ETHKeyResource) // Use the functional options to inject the ETH and LINK balances func NewETHKeyResource(k ethkey.KeyV2, state ethkey.State, opts ...NewETHKeyOption) *ETHKeyResource { r := ÐKeyResource{ - JAID: NewJAID(k.Address.Hex()), + JAID: NewPrefixedJAID(k.Address.Hex(), state.EVMChainID.String()), EVMChainID: state.EVMChainID, Address: k.Address.Hex(), EthBalance: nil, diff --git a/core/web/presenters/eth_key_test.go b/core/web/presenters/eth_key_test.go index 8be13de74a1..46402141a4c 100644 --- a/core/web/presenters/eth_key_test.go +++ b/core/web/presenters/eth_key_test.go @@ -55,7 +55,7 @@ func TestETHKeyResource(t *testing.T) { { "data":{ "type":"eTHKeys", - "id":"%s", + "id":"42/%s", "attributes":{ "address":"%s", "evmChainID":"42", @@ -84,7 +84,7 @@ func TestETHKeyResource(t *testing.T) { { "data": { "type":"eTHKeys", - "id":"%s", + "id":"42/%s", "attributes":{ "address":"%s", "evmChainID":"42", diff --git a/core/web/presenters/eth_tx.go b/core/web/presenters/eth_tx.go index f944a99213f..65df01ef095 100644 --- a/core/web/presenters/eth_tx.go +++ b/core/web/presenters/eth_tx.go @@ -66,6 +66,7 @@ func NewEthTxResourceFromAttempt(txa txmgr.TxAttempt) EthTxResource { if txa.Tx.ChainID != nil { r.EVMChainID = *big.New(txa.Tx.ChainID) + r.JAID = NewPrefixedJAID(r.JAID.ID, txa.Tx.ChainID.String()) } if tx.Sequence != nil { diff --git a/core/web/presenters/eth_tx_test.go b/core/web/presenters/eth_tx_test.go index 2ed8e23c76a..193fa774ce9 100644 --- a/core/web/presenters/eth_tx_test.go +++ b/core/web/presenters/eth_tx_test.go @@ -20,12 +20,14 @@ import ( func TestEthTxResource(t *testing.T) { t.Parallel() + chainID := big.NewInt(54321) tx := txmgr.Tx{ ID: 1, EncodedPayload: []byte(`{"data": "is wilding out"}`), FromAddress: common.HexToAddress("0x1"), ToAddress: common.HexToAddress("0x2"), FeeLimit: uint32(5000), + ChainID: chainID, State: txmgrcommon.TxConfirmed, Value: big.Int(assets.NewEthValue(1)), } @@ -52,7 +54,7 @@ func TestEthTxResource(t *testing.T) { "sentAt": "", "to": "0x0000000000000000000000000000000000000002", "value": "0.000000000000000001", - "evmChainID": "0" + "evmChainID": "54321" } } } @@ -85,7 +87,7 @@ func TestEthTxResource(t *testing.T) { { "data": { "type": "evm_transactions", - "id": "0x0000000000000000000000000000000000000000000000000000000000010203", + "id": "54321/0x0000000000000000000000000000000000000000000000000000000000010203", "attributes": { "state": "confirmed", "data": "0x7b2264617461223a202269732077696c64696e67206f7574227d", @@ -98,7 +100,7 @@ func TestEthTxResource(t *testing.T) { "sentAt": "300", "to": "0x0000000000000000000000000000000000000002", "value": "0.000000000000000001", - "evmChainID": "0" + "evmChainID": "54321" } } } diff --git a/core/web/presenters/evm_chain.go b/core/web/presenters/evm_chain.go index 8cc6da46a77..adf399d4b01 100644 --- a/core/web/presenters/evm_chain.go +++ b/core/web/presenters/evm_chain.go @@ -34,7 +34,7 @@ func (r EVMNodeResource) GetName() string { // NewEVMNodeResource returns a new EVMNodeResource for node. func NewEVMNodeResource(node types.NodeStatus) EVMNodeResource { return EVMNodeResource{NodeResource{ - JAID: NewJAID(node.Name), + JAID: NewPrefixedJAID(node.Name, node.ChainID), ChainID: node.ChainID, Name: node.Name, State: node.State, diff --git a/core/web/presenters/evm_forwarder_test.go b/core/web/presenters/evm_forwarder_test.go new file mode 100644 index 00000000000..80eb6b190ef --- /dev/null +++ b/core/web/presenters/evm_forwarder_test.go @@ -0,0 +1,64 @@ +package presenters + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/manyminds/api2go/jsonapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/forwarders" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" +) + +func TestEVMForwarderResource(t *testing.T) { + var ( + ID = int64(1) + address = utils.RandomAddress() + chainID = *big.NewI(4) + createdAt = time.Now() + updatedAt = time.Now().Add(time.Second) + ) + fwd := forwarders.Forwarder{ + ID: ID, + Address: address, + EVMChainID: chainID, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + r := NewEVMForwarderResource(fwd) + assert.Equal(t, fmt.Sprint(ID), r.ID) + assert.Equal(t, address, r.Address) + assert.Equal(t, chainID, r.EVMChainID) + assert.Equal(t, createdAt, r.CreatedAt) + assert.Equal(t, updatedAt, r.UpdatedAt) + + b, err := jsonapi.Marshal(r) + require.NoError(t, err) + + createdAtMarshalled, err := createdAt.MarshalText() + require.NoError(t, err) + updatedAtMarshalled, err := updatedAt.MarshalText() + require.NoError(t, err) + + expected := fmt.Sprintf(` + { + "data":{ + "type":"evm_forwarder", + "id":"%d", + "attributes":{ + "address":"%s", + "evmChainId":"%s", + "createdAt":"%s", + "updatedAt":"%s" + } + } + } + `, ID, strings.ToLower(address.String()), chainID.String(), string(createdAtMarshalled), string(updatedAtMarshalled)) + assert.JSONEq(t, expected, string(b)) +} diff --git a/core/web/presenters/job.go b/core/web/presenters/job.go index 6b0293665df..7c8643015dd 100644 --- a/core/web/presenters/job.go +++ b/core/web/presenters/job.go @@ -517,6 +517,8 @@ func NewJobResource(j job.Job) *JobResource { resource.GatewaySpec = NewGatewaySpec(j.GatewaySpec) case job.Stream: // no spec; nothing to do + case job.Workflow: + // no spec; nothing to do case job.LegacyGasStationServer, job.LegacyGasStationSidecar: // unsupported } diff --git a/core/web/presenters/jsonapi.go b/core/web/presenters/jsonapi.go index ee3a2a7de8a..d14e24a7455 100644 --- a/core/web/presenters/jsonapi.go +++ b/core/web/presenters/jsonapi.go @@ -1,6 +1,7 @@ package presenters import ( + "fmt" "strconv" ) @@ -14,6 +15,11 @@ func NewJAID(id string) JAID { return JAID{id} } +// NewPrefixedJAID prefixes JAID with chain id in %s/%s format. +func NewPrefixedJAID(id string, chainID string) JAID { + return JAID{ID: fmt.Sprintf("%s/%s", chainID, id)} +} + // NewJAIDInt32 converts an int32 into a JAID func NewJAIDInt32(id int32) JAID { return JAID{strconv.Itoa(int(id))} diff --git a/core/web/presenters/node_test.go b/core/web/presenters/node_test.go new file mode 100644 index 00000000000..34210a52166 --- /dev/null +++ b/core/web/presenters/node_test.go @@ -0,0 +1,92 @@ +package presenters + +import ( + "fmt" + "testing" + + "github.com/manyminds/api2go/jsonapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +func TestNodeResource(t *testing.T) { + var nodeResource NodeResource + var r interface{} + state := "test" + cfg := "cfg" + testCases := []string{"solana", "cosmos", "starknet"} + for _, tc := range testCases { + chainID := fmt.Sprintf("%s chain ID", tc) + nodeName := fmt.Sprintf("%s_node", tc) + + switch tc { + case "evm": + evmNodeResource := NewEVMNodeResource( + types.NodeStatus{ + ChainID: chainID, + Name: nodeName, + Config: cfg, + State: state, + }) + r = evmNodeResource + nodeResource = evmNodeResource.NodeResource + case "solana": + solanaNodeResource := NewSolanaNodeResource( + types.NodeStatus{ + ChainID: chainID, + Name: nodeName, + Config: cfg, + State: state, + }) + r = solanaNodeResource + nodeResource = solanaNodeResource.NodeResource + case "cosmos": + cosmosNodeResource := NewCosmosNodeResource( + types.NodeStatus{ + ChainID: chainID, + Name: nodeName, + Config: cfg, + State: state, + }) + r = cosmosNodeResource + nodeResource = cosmosNodeResource.NodeResource + case "starknet": + starknetNodeResource := NewStarkNetNodeResource( + types.NodeStatus{ + ChainID: chainID, + Name: nodeName, + Config: cfg, + State: state, + }) + r = starknetNodeResource + nodeResource = starknetNodeResource.NodeResource + default: + t.Fail() + } + assert.Equal(t, chainID, nodeResource.ChainID) + assert.Equal(t, nodeName, nodeResource.Name) + assert.Equal(t, cfg, nodeResource.Config) + assert.Equal(t, state, nodeResource.State) + + b, err := jsonapi.Marshal(r) + require.NoError(t, err) + + expected := fmt.Sprintf(` + { + "data":{ + "type":"%s_node", + "id":"%s/%s", + "attributes":{ + "chainID":"%s", + "name":"%s", + "config":"%s", + "state":"%s" + } + } + } + `, tc, chainID, nodeName, chainID, nodeName, cfg, state) + assert.JSONEq(t, expected, string(b)) + } +} diff --git a/core/web/presenters/solana_chain.go b/core/web/presenters/solana_chain.go index f04d2b65d55..798d98124a5 100644 --- a/core/web/presenters/solana_chain.go +++ b/core/web/presenters/solana_chain.go @@ -36,7 +36,7 @@ func (r SolanaNodeResource) GetName() string { // NewSolanaNodeResource returns a new SolanaNodeResource for node. func NewSolanaNodeResource(node types.NodeStatus) SolanaNodeResource { return SolanaNodeResource{NodeResource{ - JAID: NewJAID(node.Name), + JAID: NewPrefixedJAID(node.Name, node.ChainID), ChainID: node.ChainID, Name: node.Name, State: node.State, diff --git a/core/web/presenters/solana_msg.go b/core/web/presenters/solana_msg.go index b7330754e38..3acf2aac0dc 100644 --- a/core/web/presenters/solana_msg.go +++ b/core/web/presenters/solana_msg.go @@ -17,7 +17,7 @@ func (SolanaMsgResource) GetName() string { // NewSolanaMsgResource returns a new partial SolanaMsgResource. func NewSolanaMsgResource(id string, chainID string) SolanaMsgResource { return SolanaMsgResource{ - JAID: NewJAID(id), + JAID: NewPrefixedJAID(id, chainID), ChainID: chainID, } } diff --git a/core/web/presenters/starknet_chain.go b/core/web/presenters/starknet_chain.go index ec1cd453a55..addf798fe9f 100644 --- a/core/web/presenters/starknet_chain.go +++ b/core/web/presenters/starknet_chain.go @@ -36,7 +36,7 @@ func (r StarkNetNodeResource) GetName() string { // NewStarkNetNodeResource returns a new StarkNetNodeResource for node. func NewStarkNetNodeResource(node types.NodeStatus) StarkNetNodeResource { return StarkNetNodeResource{NodeResource{ - JAID: NewJAID(node.Name), + JAID: NewPrefixedJAID(node.Name, node.ChainID), ChainID: node.ChainID, Name: node.Name, State: node.State, diff --git a/integration-tests/actions/vrf/common/actions.go b/integration-tests/actions/vrf/common/actions.go index ec7972de597..0c779ea90e5 100644 --- a/integration-tests/actions/vrf/common/actions.go +++ b/integration-tests/actions/vrf/common/actions.go @@ -54,8 +54,8 @@ func CreateAndFundSendingKeys( if response.StatusCode != 200 { return nil, fmt.Errorf("error creating transaction key - response code, err %d", response.StatusCode) } - newNativeTokenKeyAddresses = append(newNativeTokenKeyAddresses, newTxKey.Data.ID) - err = actions.FundAddress(client, newTxKey.Data.ID, big.NewFloat(chainlinkNodeFunding)) + newNativeTokenKeyAddresses = append(newNativeTokenKeyAddresses, newTxKey.Data.Attributes.Address) + err = actions.FundAddress(client, newTxKey.Data.Attributes.Address, big.NewFloat(chainlinkNodeFunding)) if err != nil { return nil, err } diff --git a/integration-tests/client/chainlink_models.go b/integration-tests/client/chainlink_models.go index 320c7a21cc4..370497423f3 100644 --- a/integration-tests/client/chainlink_models.go +++ b/integration-tests/client/chainlink_models.go @@ -380,8 +380,8 @@ type TxKeyData struct { // TxKeyAttributes is the model that represents the created keys when read type TxKeyAttributes struct { PublicKey string `json:"publicKey"` - - StarkKey string `json:"starkPubKey,omitempty"` + Address string `json:"address"` + StarkKey string `json:"starkPubKey,omitempty"` } type SingleTransactionDataWrapper struct { diff --git a/integration-tests/smoke/vrfv2_test.go b/integration-tests/smoke/vrfv2_test.go index c289cd019c1..e0c304a3951 100644 --- a/integration-tests/smoke/vrfv2_test.go +++ b/integration-tests/smoke/vrfv2_test.go @@ -543,7 +543,7 @@ func TestVRFv2MultipleSendingKeys(t *testing.T) { require.Equal(t, numberOfTxKeysToCreate+1, len(fulfillmentTxFromAddresses)) var txKeyAddresses []string for _, txKey := range txKeys.Data { - txKeyAddresses = append(txKeyAddresses, txKey.ID) + txKeyAddresses = append(txKeyAddresses, txKey.Attributes.Address) } less := func(a, b string) bool { return a < b } equalIgnoreOrder := cmp.Diff(txKeyAddresses, fulfillmentTxFromAddresses, cmpopts.SortSlices(less)) == "" diff --git a/integration-tests/smoke/vrfv2plus_test.go b/integration-tests/smoke/vrfv2plus_test.go index 701cae9a027..29cc5534791 100644 --- a/integration-tests/smoke/vrfv2plus_test.go +++ b/integration-tests/smoke/vrfv2plus_test.go @@ -734,7 +734,7 @@ func TestVRFv2PlusMultipleSendingKeys(t *testing.T) { require.Equal(t, numberOfTxKeysToCreate+1, len(fulfillmentTxFromAddresses)) var txKeyAddresses []string for _, txKey := range txKeys.Data { - txKeyAddresses = append(txKeyAddresses, txKey.ID) + txKeyAddresses = append(txKeyAddresses, txKey.Attributes.Address) } less := func(a, b string) bool { return a < b } equalIgnoreOrder := cmp.Diff(txKeyAddresses, fulfillmentTxFromAddresses, cmpopts.SortSlices(less)) == ""