diff --git a/relayer/chainreader/config/config.go b/relayer/chainreader/config/config.go index c1c349dd1..cff67bf52 100644 --- a/relayer/chainreader/config/config.go +++ b/relayer/chainreader/config/config.go @@ -35,9 +35,17 @@ type ChainReaderFunction struct { ResultTupleToStruct []string // Defines a mapping for renaming response fields ResultFieldRenames map[string]aptosCRConfig.RenamedField - // Static response + // Static response, returned as-is to mimic a response from the contract + // The contract can be entirely virtual as long as .Bind() is called with it. StaticResponse []any - // Response from inputs + // Response from inputs, can reference parameters, package ID (with or without module name, same package), or pointer objects + // Example: ["package_id", "package_id.ANOTHER_MODULE", "params.SOME_PARAM_NAME"] + // If the `package_id` is used without a module name, the latest package ID of the module for this function is returned. + // If the `package_id` is used with a module name, the latest package ID of the module is returned. + // * This might look the same as just `package_id`, however, getting the latest package ID is only possible within a specific module. + // * This is helpful for virtual dependencies between modules (e.g. RMNRemote -> CCIP latest package ID from `state_object` module) + // If the `params.counter_id` is used, the value of the counter object id is returned. + // * The `counter_id` parameter must be specified in the function config. ResponseFromInputs []string } diff --git a/relayer/chainreader/reader/chainreader.go b/relayer/chainreader/reader/chainreader.go index eefc77c99..42b49de3b 100644 --- a/relayer/chainreader/reader/chainreader.go +++ b/relayer/chainreader/reader/chainreader.go @@ -935,14 +935,57 @@ func (s *suiChainReader) executeFunction(ctx context.Context, parsed *readIdenti if len(functionConfig.StaticResponse) > 0 { return functionConfig.StaticResponse, nil } else if len(functionConfig.ResponseFromInputs) > 0 { + response := make([]any, 0) + for _, pluckFromInput := range functionConfig.ResponseFromInputs { - switch pluckFromInput { + pluckParts := strings.Split(pluckFromInput, ".") + + // if the pluckFromInput is empty, skip + if len(pluckParts) == 0 { + continue + } + + switch pluckParts[0] { case "package_id": - return []any{latestPackageId}, nil + // if there are no more parts, return the package ID of the module for this function + if len(pluckParts) == 1 { + response = append(response, latestPackageId) + continue + } + + // if there are more parts, return the package ID of the module (must be within the same package) + // this is useful in cases where getting the latest package ID is only possible within a single module + // that is different from the current module (e.g. RMNRemote -> CCIP latest package ID from `state_object` module) + moduleName := pluckParts[1] + modulePackageId, err := s.client.GetLatestPackageId(ctx, parsed.address, moduleName) + if err != nil { + s.logger.Debugw("Failed to get latest package ID for module", "moduleName", moduleName, "error", err) + // fallback to the latest package ID of the current module + response = append(response, latestPackageId) + continue + } + response = append(response, modulePackageId) + continue + case "params": + if len(pluckParts) != 2 { + continue + } + + // match the param name to the arg index + for i, param := range functionConfig.Params { + if param.Name == pluckParts[1] { + response = append(response, args[i]) + } + } + + // Not found + continue default: return nil, fmt.Errorf("unknown response from inputs selection: %s", pluckFromInput) } } + + return response, nil } values, err := s.client.ReadFunction(ctx, functionConfig.SignerAddress, parsed.address, parsed.contractName, parsed.readName, args, argTypes, typeArgs) diff --git a/relayer/chainreader/reader/chainreader_local_test.go b/relayer/chainreader/reader/chainreader_local_test.go index 04b23bf6d..7454baf09 100644 --- a/relayer/chainreader/reader/chainreader_local_test.go +++ b/relayer/chainreader/reader/chainreader_local_test.go @@ -303,6 +303,20 @@ func runChainReaderCounterTest(t *testing.T, log logger.Logger, rpcUrl string) { Params: []codec.SuiFunctionParam{}, ResponseFromInputs: []string{"package_id"}, }, + "response_from_inputs_with_params": { + Name: "response_from_inputs_with_params", + SignerAddress: accountAddress, + Params: []codec.SuiFunctionParam{ + { + Type: "object_id", + Name: "counter_id", + PointerTag: pointerTag, + Required: true, + }, + }, + ResponseFromInputs: []string{"params.counter_id", "package_id"}, + ResultTupleToStruct: []string{"counter_id", "package_id"}, + }, }, Events: map[string]*config.ChainReaderEvent{ "counter_incremented": { @@ -421,6 +435,16 @@ func runChainReaderCounterTest(t *testing.T, log logger.Logger, rpcUrl string) { }, Events: map[string]*config.ChainReaderEvent{}, }, + "RMNProxy": { + Name: "rmn_proxy", + Functions: map[string]*config.ChainReaderFunction{ + "get_arm": { + Name: "get_arm", + SignerAddress: accountAddress, + ResponseFromInputs: []string{"package_id.state_object"}, + }, + }, + }, "FeeQuoter": { Name: "fee_quoter", Functions: map[string]*config.ChainReaderFunction{ @@ -487,6 +511,11 @@ func runChainReaderCounterTest(t *testing.T, log logger.Logger, rpcUrl string) { Address: packageId, // Package ID of the deployed fee_quoter contract } + rmnProxyBinding := types.BoundContract{ + Name: "RMNProxy", + Address: secondaryPackageId, // Package ID of the deployed rmn_proxy contract + } + datastoreUrl := os.Getenv("TEST_DB_URL") if datastoreUrl == "" { t.Skip("Skipping persistent tests as TEST_DB_URL is not set in CI") @@ -527,7 +556,7 @@ func runChainReaderCounterTest(t *testing.T, log logger.Logger, rpcUrl string) { chainReader, err := NewChainReader(ctx, log, relayerClient, chainReaderConfig, db, indexerInstance) require.NoError(t, err) - err = chainReader.Bind(context.Background(), []types.BoundContract{counterBinding, offRampBinding, onRampBinding, routerBinding, feeQuoterBinding}) + err = chainReader.Bind(context.Background(), []types.BoundContract{counterBinding, offRampBinding, onRampBinding, routerBinding, feeQuoterBinding, rmnProxyBinding}) require.NoError(t, err) go func() { @@ -1514,4 +1543,36 @@ func runChainReaderCounterTest(t *testing.T, log logger.Logger, rpcUrl string) { testutils.PrettyPrintDebug(log, retStaticResponse, "retStaticResponse") require.Equal(t, map[string]any{"a": 1, "b": 2, "c": 3}, retStaticResponse, "Expected static response to be map[string]any with keys a, b, and c") }) + + t.Run("GetLatestValue_ResponseFromInputsWithParams", func(t *testing.T) { + var retResponseFromInputs any + params := map[string]any{} + + err = chainReader.GetLatestValue( + context.Background(), + strings.Join([]string{packageId, "Counter", "response_from_inputs_with_params"}, "-"), + primitives.Finalized, + ¶ms, // no parameters needed + &retResponseFromInputs, + ) + require.NoError(t, err) + testutils.PrettyPrintDebug(log, retResponseFromInputs, "retResponseFromInputs") + require.Equal(t, map[string]any{"counter_id": counterObjectId, "package_id": packageId}, retResponseFromInputs, "Expected response to be the counter object id and package id") + }) + + t.Run("GetLatestValue_ResponseFromInputsWithModulePackageId", func(t *testing.T) { + var retResponseFromInputs any + params := map[string]any{} + + err = chainReader.GetLatestValue( + context.Background(), + strings.Join([]string{secondaryPackageId, "RMNProxy", "get_arm"}, "-"), + primitives.Finalized, + ¶ms, // no parameters needed + &retResponseFromInputs, + ) + require.NoError(t, err) + testutils.PrettyPrintDebug(log, retResponseFromInputs, "retResponseFromInputs") + require.Equal(t, secondaryPackageId, retResponseFromInputs, "Expected response to be the package id") + }) }