diff --git a/plugins/plugins.public.yaml b/plugins/plugins.public.yaml index 66446d206f9..4ff70e202c8 100644 --- a/plugins/plugins.public.yaml +++ b/plugins/plugins.public.yaml @@ -10,7 +10,7 @@ defaults: plugins: aptos: - moduleURI: "github.com/smartcontractkit/chainlink-aptos" - gitRef: "v0.0.0-20260828090428-9828cc37ebc2" + gitRef: "v0.0.0-20261015141323-398fb19195f8" installPath: "./cmd/chainlink-aptos" sui: @@ -94,7 +94,7 @@ plugins: capability-aptos: - moduleURI: "github.com/smartcontractkit/capabilities/chain_capabilities/aptos" - gitRef: "8c63e69b4e241875e7e111d6e85d8d0596f22981" + gitRef: "9e93a8af323420aa671705eb619c7ea533041036" installPath: "." capability-stellar: diff --git a/system-tests/tests/smoke/cre/aptos/aptosread/config/config.go b/system-tests/tests/smoke/cre/aptos/aptosread/config/config.go index a6b16c256db..14c88e40f62 100644 --- a/system-tests/tests/smoke/cre/aptos/aptosread/config/config.go +++ b/system-tests/tests/smoke/cre/aptos/aptosread/config/config.go @@ -5,4 +5,7 @@ type Config struct { ChainSelector uint64 `yaml:"chainSelector"` WorkflowName string `yaml:"workflowName"` ExpectedCoinName string `yaml:"expectedCoinName"` // expected exact value in the View reply data (e.g. "Aptos Coin" for 0x1::coin::name()) + // ExpectUserError switches the workflow to call View on a module that doesn't exist and assert + // the capability classifies the resulting Aptos node 400 as a user error. + ExpectUserError bool `yaml:"expectUserError"` } diff --git a/system-tests/tests/smoke/cre/aptos/aptosread/main.go b/system-tests/tests/smoke/cre/aptos/aptosread/main.go index 9a3ad82fc1d..c9fac063072 100644 --- a/system-tests/tests/smoke/cre/aptos/aptosread/main.go +++ b/system-tests/tests/smoke/cre/aptos/aptosread/main.go @@ -59,6 +59,10 @@ func onAptosReadTrigger(cfg config.Config, runtime sdk.Runtime, payload *cron.Pa }() client := aptos.Client{ChainSelector: cfg.ChainSelector} + if cfg.ExpectUserError { + return runViewUserErrorCheck(cfg, runtime, client) + } + reply, err := client.View(runtime, &aptos.ViewRequest{ Payload: &aptos.ViewPayload{ Module: &aptos.ModuleID{ @@ -101,6 +105,36 @@ func onAptosReadTrigger(cfg config.Config, runtime sdk.Runtime, payload *cron.Pa return nil, nil } +// userErrorPrefix is how the host serialises a public user-origin capability error for the guest +// (visibility:origin:code:message). +const userErrorPrefix = "Public:User:" + +// runViewUserErrorCheck calls View on a module that doesn't exist under 0x1. The Aptos node rejects +// it with HTTP 400 (Linker Error), which the capability must surface as a user error. +func runViewUserErrorCheck(cfg config.Config, runtime sdk.Runtime, client aptos.Client) (any, error) { + _, err := client.View(runtime, &aptos.ViewRequest{ + Payload: &aptos.ViewPayload{ + Module: &aptos.ModuleID{ + Address: []byte{0x1}, + Name: "cre_e2e_missing_module", + }, + Function: "missing", + }, + }).Await() + if err == nil { + runtime.Logger().Info("Aptos view user error test failed: expected error, got reply", "workflow", cfg.WorkflowName) + return nil, errors.New("expected View on missing module to fail") + } + if !strings.HasPrefix(err.Error(), userErrorPrefix) { + msg := fmt.Sprintf("Aptos view user error test failed: unexpected classification: %v", err) + runtime.Logger().Info(msg, "workflow", cfg.WorkflowName) + return nil, fmt.Errorf("expected user error, got: %w", err) + } + + runtime.Logger().Info("Aptos view user error classified as expected", "error", err.Error(), "workflow", cfg.WorkflowName) + return nil, nil +} + func parseSingleStringViewReply(data []byte) (string, error) { var values []string if err := json.Unmarshal(data, &values); err != nil { diff --git a/system-tests/tests/smoke/cre/aptos_capability_test.go b/system-tests/tests/smoke/cre/aptos_capability_test.go index de5a0b9fec2..49206fca1da 100644 --- a/system-tests/tests/smoke/cre/aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/aptos_capability_test.go @@ -85,6 +85,7 @@ func aptosDefaultScenarios() []aptosScenario { return []aptosScenario{ {name: "Aptos Write Read Roundtrip", requiresWrite: true, run: ExecuteAptosWriteReadRoundtripTest}, {name: "Aptos Write Expected Failure", requiresWrite: true, run: ExecuteAptosWriteExpectedFailureTest}, + {name: "Aptos View User Error", requiresWrite: false, run: ExecuteAptosViewUserErrorTest}, } } @@ -121,6 +122,11 @@ func resolveAptosScenarios(t *testing.T) []aptosScenario { requiresWrite: true, run: ExecuteAptosWriteExpectedFailureTest, }, + "view-user-error": { + name: "Aptos View User Error", + requiresWrite: false, + run: ExecuteAptosViewUserErrorTest, + }, } parts := strings.Split(raw, ",") @@ -274,6 +280,35 @@ func ExecuteAptosReadTest( lggr.Info().Str("expected_log", expectedLog).Msg("Aptos read capability test passed") } +// ExecuteAptosViewUserErrorTest deploys the read workflow in user-error mode: it calls View on a +// module that doesn't exist, which the Aptos node rejects with HTTP 400, and asserts the workflow +// receives it as a user-origin capability error rather than a system error. +func ExecuteAptosViewUserErrorTest( + t *testing.T, + tenv *configuration.TestEnvironment, + aptosChain blockchains.Blockchain, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, +) { + lggr := framework.L + + ensureAptosLedgerVersionPositive(t, aptosChain) + + workflowName := uniqueAptosWorkflowName("aptos-view-user-error-workflow") + workflowConfig := t_helpers.AptosReadWorkflowConfig{ + ChainSelector: aptosChain.ChainSelector(), + WorkflowName: workflowName, + ExpectUserError: true, + } + + const workflowFileLocation = "./aptos/aptosread/main.go" + workflowID := t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) + + expectedLog := "Aptos view user error classified as expected" + t_helpers.WatchWorkflowLogs(t, lggr, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, expectedLog, aptosWorkflowTimeout, t_helpers.WithUserLogWorkflowID(workflowID)) + lggr.Info().Str("expected_log", expectedLog).Msg("Aptos view user error classification test passed") +} + // The local Aptos devnet can legitimately remain at ledger version 0 until the // first Aptos transaction lands, which makes the read capability reject the // consensus height before the workflow has a chance to execute. diff --git a/system-tests/tests/test-helpers/t_helpers.go b/system-tests/tests/test-helpers/t_helpers.go index 99c44badd6a..7f114801391 100644 --- a/system-tests/tests/test-helpers/t_helpers.go +++ b/system-tests/tests/test-helpers/t_helpers.go @@ -358,6 +358,7 @@ type AptosReadWorkflowConfig struct { ChainSelector uint64 `yaml:"chainSelector"` WorkflowName string `yaml:"workflowName"` ExpectedCoinName string `yaml:"expectedCoinName"` + ExpectUserError bool `yaml:"expectUserError"` } // StellarReadWorkflowConfig mirrors the fields of the stellarread workflow's