diff --git a/.golangci.yml b/.golangci.yml index 84bc57d0f0..2dc226cb44 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -102,6 +102,8 @@ linters: - perfsprint # malfunctions on embedded structs - typecheck + # magic numbers + - mnd fast: false issues: diff --git a/core/go.mod b/core/go.mod index 4f6e5bffcd..1143e38c1d 100644 --- a/core/go.mod +++ b/core/go.mod @@ -63,6 +63,7 @@ require ( go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/sync v0.8.0 + google.golang.org/grpc v1.64.0 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.10 k8s.io/apimachinery v0.29.3 @@ -189,7 +190,6 @@ require ( golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/grpc v1.64.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/core/metrics/README.md b/core/metrics/README.md index 1289afe41e..5f60dfbfe6 100644 --- a/core/metrics/README.md +++ b/core/metrics/README.md @@ -12,7 +12,27 @@ There's also a `NAME_PREFIX` environment variable that will prefix all the metri ## OTLP -We do our best to support enviornment variables specified in the [Otel Spec](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) and have added a few of our own. Key ones to note are: +We do our best to support enviornment variables specified in the [Otel Spec](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) and the [OTLP Spec](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/) and have added a few of our own. This was to allow for multiple exporter backends for traces, as otel clients only allow for one URL. The relevant multi exporter code is in `multiexporter.go`, and simply wraps multiple otel clients. + +The additional environment variables to note are: +| Enviornment Variable | Description | Default | +|------------------------------------------|-------------------------------------------|---------| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | The endpoint for the primary OTLP exporter | None | +| `OTEL_EXPORTER_OTLP_ENDPOINT_1` | The endpoint for the first additional OTLP exporter | None | +| `OTEL_EXPORTER_OTLP_ENDPOINT_2` | The endpoint for the second additional OTLP exporter | None | +| `OTEL_EXPORTER_OTLP_ENDPOINT_3` | The endpoint for the third additional OTLP exporter | None | +| ... | Additional endpoints can be added by incrementing the number | None | +| `OTEL_EXPORTER_OTLP_TRANSPORT` | The transport protocol for the primary OTLP exporter | `http` | +| `OTEL_EXPORTER_OTLP_TRANSPORT_1` | The transport protocol for the first additional OTLP exporter | `http` | +| `OTEL_EXPORTER_OTLP_TRANSPORT_2` | The transport protocol for the second additional OTLP exporter | `http` | +| `OTEL_EXPORTER_OTLP_TRANSPORT_3` | The transport protocol for the third additional OTLP exporter | `http` | +| ... | Additional transports can be specified by incrementing the number | `http` | + +You can do the same thing for `OTEL_EXPORTER_OTLP_SECURE_MODE` and `OTEL_EXPORTER_OTLP_HEADERS` + + + +Note: The OTLP exporter endpoints and transports can be specified for multiple exporters by using incrementing numbers (1, 2, 3, etc.) in the environment variable names. This allows for configuration of multiple OTLP exporters. The primary exporter uses the base names without numbers. ## Jaeger diff --git a/core/metrics/export_test.go b/core/metrics/export_test.go new file mode 100644 index 0000000000..6a25651c4c --- /dev/null +++ b/core/metrics/export_test.go @@ -0,0 +1,6 @@ +package metrics + +// HeadersToMap converts a string of headers to a map. +func HeadersToMap(val string) map[string]string { + return headersToMap(val) +} diff --git a/core/metrics/multiexporter.go b/core/metrics/multiexporter.go new file mode 100644 index 0000000000..cbd59495e4 --- /dev/null +++ b/core/metrics/multiexporter.go @@ -0,0 +1,83 @@ +package metrics + +import ( + "context" + "fmt" + "go.uber.org/multierr" + "sync" + "time" + + "go.opentelemetry.io/otel/sdk/trace" +) + +// MultiExporter is an interface that allows exporting spans to multiple OTLP trace exporters. +type MultiExporter interface { + trace.SpanExporter + AddExporter(exporter trace.SpanExporter) +} + +type multiExporter struct { + exporters []trace.SpanExporter +} + +// NewMultiExporter creates a new multi exporter that forwards spans to multiple OTLP trace exporters. +// It takes in one or more trace.SpanExporter instances and ensures that spans are sent to all of them. +// This is useful when you need to send trace data to multiple backends or endpoints. +func NewMultiExporter(exporters ...trace.SpanExporter) MultiExporter { + return &multiExporter{ + exporters: exporters, + } +} + +const defaultTimeout = 30 * time.Second + +// ExportSpans exports a batch of spans. +func (m *multiExporter) ExportSpans(parentCtx context.Context, ss []trace.ReadOnlySpan) error { + return m.doParallel(parentCtx, func(ctx context.Context, exporter trace.SpanExporter) error { + return exporter.ExportSpans(ctx, ss) + }) +} + +func (m *multiExporter) doParallel(parentCtx context.Context, fn func(context.Context, trace.SpanExporter) error) error { + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + var wg sync.WaitGroup + var errors []error + var mu sync.Mutex + + wg.Add(len(m.exporters)) + for _, exporter := range m.exporters { + go func(exporter trace.SpanExporter) { + defer wg.Done() + err := fn(ctx, exporter) + if err != nil { + mu.Lock() + errors = append(errors, fmt.Errorf("error in doMultiple: %w", err)) + mu.Unlock() + } + }(exporter) + } + + wg.Wait() + if len(errors) > 0 { + // nolint: wrapcheck + return multierr.Combine(errors...) + } + + return nil +} + +// Shutdown notifies the exporter of a pending halt to operations. +func (m *multiExporter) Shutdown(ctx context.Context) error { + return m.doParallel(ctx, func(ctx context.Context, exporter trace.SpanExporter) error { + return exporter.Shutdown(ctx) + }) +} + +// AddExporter adds an exporter to the multi exporter. +func (m *multiExporter) AddExporter(exporter trace.SpanExporter) { + m.exporters = append(m.exporters, exporter) +} + +var _ trace.SpanExporter = &multiExporter{} diff --git a/core/metrics/multiexporter_test.go b/core/metrics/multiexporter_test.go new file mode 100644 index 0000000000..0c0d6f0310 --- /dev/null +++ b/core/metrics/multiexporter_test.go @@ -0,0 +1,45 @@ +package metrics_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/synapsecns/sanguine/core/metrics" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" +) + +func TestMultiExporter(t *testing.T) { + // Create in-memory exporters + exporter1 := tracetest.NewInMemoryExporter() + exporter2 := tracetest.NewInMemoryExporter() + + // Create multi-exporter + multiExporter := metrics.NewMultiExporter(exporter1, exporter2) + + // Create test spans + spans := []sdktrace.ReadOnlySpan{ + tracetest.SpanStub{}.Snapshot(), + tracetest.SpanStub{}.Snapshot(), + } + + // Test ExportSpans + err := multiExporter.ExportSpans(context.Background(), spans) + require.NoError(t, err) + + // Verify that spans were exported to both exporters + assert.Equal(t, 2, len(exporter1.GetSpans())) + assert.Equal(t, 2, len(exporter2.GetSpans())) + + // Test Shutdown + err = multiExporter.Shutdown(context.Background()) + require.NoError(t, err) + + // Verify that both exporters were shut down + // Note: InMemoryExporter doesn't have a Stopped() method, so we can't check this directly + // Instead, we can try to export spans again and check for an error + err = multiExporter.ExportSpans(context.Background(), spans) + assert.NoError(t, err, "Expected no error after shutdown") +} diff --git a/core/metrics/otlp.go b/core/metrics/otlp.go index f96f06dd43..c5e2a7c236 100644 --- a/core/metrics/otlp.go +++ b/core/metrics/otlp.go @@ -2,16 +2,18 @@ package metrics import ( "context" + "crypto/tls" "fmt" + "google.golang.org/grpc/credentials" + "strings" + "time" + "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/core/config" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" tracesdk "go.opentelemetry.io/otel/sdk/trace" - "os" - "strings" - "time" ) type otlpHandler struct { @@ -28,23 +30,45 @@ func NewOTLPMetricsHandler(buildInfo config.BuildInfo) Handler { } func (n *otlpHandler) Start(ctx context.Context) (err error) { - var client otlptrace.Client - transport := transportFromString(core.GetEnv(otlpTransportEnv, otlpTransportGRPC.String())) - switch transport { - case otlpTransportHTTP: - client = otlptracehttp.NewClient() - case otlpTransportGRPC: - client = otlptracegrpc.NewClient() - default: - return fmt.Errorf("unknown transport type: %s", os.Getenv(otlpTransportEnv)) - } + var exporters []tracesdk.SpanExporter - exporter, err := otlptrace.New(ctx, client) + primaryExporter, err := makeOTLPExporter(ctx, "") if err != nil { - return fmt.Errorf("failed to create otlp exporter: %w", err) + return fmt.Errorf("could not create default client: %w", err) + } + exporters = append(exporters, primaryExporter) + + // Loop to create additional exporters + for i := 1; ; i++ { + envSuffix := fmt.Sprintf("_%d", i) + // if this is empty we can assume no config exists at all. + endpointEnv := otelEndpointEnv + envSuffix + + // no more transports to add. + if !core.HasEnv(endpointEnv) { + break + } + + exporter, err := makeOTLPExporter(ctx, envSuffix) + if err != nil { + return fmt.Errorf("could not create exporter %d: %w", i, err) + } + + exporters = append(exporters, exporter) } - n.baseHandler = newBaseHandler(n.buildInfo, tracesdk.WithBatcher(exporter, tracesdk.WithMaxQueueSize(1000000), tracesdk.WithMaxExportBatchSize(2000)), tracesdk.WithSampler(tracesdk.AlwaysSample())) + // create the multi-exporter with all the exporters + multiExporter := NewMultiExporter(exporters...) + + n.baseHandler = newBaseHandler( + n.buildInfo, + tracesdk.WithBatcher( + multiExporter, + tracesdk.WithMaxQueueSize(defaultMaxQueueSize), + tracesdk.WithMaxExportBatchSize(defaultMaxExportBatch), + ), + tracesdk.WithSampler(tracesdk.AlwaysSample()), + ) // start the new parent err = n.baseHandler.Start(ctx) @@ -90,7 +114,10 @@ func handleShutdown(ctx context.Context, provider *tracesdk.TracerProvider) { } const ( - otlpTransportEnv = "OTEL_EXPORTER_OTLP_TRANSPORT" + otelEndpointEnv = "OTEL_EXPORTER_OTLP_ENDPOINT" + otelTransportEnv = "OTEL_EXPORTER_OTLP_TRANSPORT" + otelInsecureEvn = "OTEL_EXPORTER_OTLP_SECURE_MODE" + otelHeadersEnv = "OTEL_EXPORTER_OTLP_HEADERS" ) //go:generate go run golang.org/x/tools/cmd/stringer -type=otlpTransportType -linecomment @@ -101,6 +128,146 @@ const ( otlpTransportGRPC // grpc ) +// getEnvSuffix returns the value of an environment variable with a suffix. +func getEnvSuffix(env, suffix, defaultRet string) string { + return core.GetEnv(makeEnv(env, suffix), defaultRet) +} + +func makeEnv(env, suffix string) string { + return env + suffix +} + +// makeOTLPTrace creates a new OTLP client based on the transport type and url. +func makeOTLPExporter(ctx context.Context, envSuffix string) (*otlptrace.Exporter, error) { + transport := transportFromString(getEnvSuffix(otelTransportEnv, envSuffix, otlpTransportGRPC.String())) + url := getEnvSuffix(otelEndpointEnv, envSuffix, "") + secure := core.GetEnvBool(makeEnv(otelInsecureEvn, envSuffix), false) + headers := getEnvSuffix(otelHeadersEnv, envSuffix, "") + + isCorrect := envSuffix != "" + + if isCorrect != secure { + return nil, fmt.Errorf("could not create exporter: secure mode is not set correctly") + } + + // I spent about 2 hours trying to figure out why this was failing to no avail. I'm going to leave it as is for now. + // My best guess is the issue is around the tsl config. + // Should you attempt to fix this and fail, please increment the counter above, although I send my umost encouragement. + if secure && transport == otlpTransportHTTP { + return nil, fmt.Errorf("could not create exporter: http transport does not support secure mode") + } + + oteltraceClient, err := buildClientFromTransport( + transport, + withURL(url), + // defaults to true + withSecure(secure), + withHeaders(headers), + ) + if err != nil { + return nil, fmt.Errorf("could not create client from transport: %w", err) + } + + exporter, err := otlptrace.New(ctx, oteltraceClient) + if err != nil { + return nil, fmt.Errorf("ocould not create client: %w", err) + } + return exporter, nil +} + +// buildClientFromTransport creates a new OTLP client based on the transport type and url. +func buildClientFromTransport(transport otlpTransportType, options ...Option) (otlptrace.Client, error) { + to := transportOptions{} + for _, option := range options { + if err := option(&to); err != nil { + return nil, fmt.Errorf("could not apply option: %w", err) + } + } + + // TODO: make sure url is validated + + switch transport { + case otlpTransportHTTP: + return otlptracehttp.NewClient(to.httpOptions...), nil + case otlpTransportGRPC: + return otlptracegrpc.NewClient(to.grpcOptions...), nil + default: + return nil, fmt.Errorf("unknown transport type: %s", transport.String()) + } +} + +type transportOptions struct { + // httpOptions are the options for the http transport. + httpOptions []otlptracehttp.Option + // grpcOptions are the options for the grpc transport. + grpcOptions []otlptracegrpc.Option +} + +// Option Each option appends the correspond option for both http and grpc options. +// only one will be used in creating the actual client. +type Option func(*transportOptions) error + +func withURL(url string) Option { + return func(o *transportOptions) error { + o.httpOptions = append(o.httpOptions, otlptracehttp.WithEndpointURL(url)) + o.grpcOptions = append(o.grpcOptions, otlptracegrpc.WithEndpointURL(url)) + + return nil + } +} + +func withSecure(secure bool) Option { + return func(o *transportOptions) error { + if secure { + tlsCreds := credentials.NewClientTLSFromCert(nil, "") + // note: you do not need to specify the tls creds for http, this happens automatically when https:// is used as the protocol scheme. + o.grpcOptions = append(o.grpcOptions, otlptracegrpc.WithTLSCredentials(tlsCreds)) + + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + // RootCAs is nil, which means the default system root CAs are used + } + o.httpOptions = append(o.httpOptions, otlptracehttp.WithTLSClientConfig(tlsConfig)) + } else { + o.httpOptions = append(o.httpOptions, otlptracehttp.WithInsecure()) + o.grpcOptions = append(o.grpcOptions, otlptracegrpc.WithInsecure()) + } + + return nil + } +} + +func withHeaders(headers string) Option { + return func(o *transportOptions) error { + realHeaders := headersToMap(headers) + o.httpOptions = append(o.httpOptions, otlptracehttp.WithHeaders(realHeaders)) + o.grpcOptions = append(o.grpcOptions, otlptracegrpc.WithHeaders(realHeaders)) + return nil + } +} + +func headersToMap(input string) map[string]string { + // Initialize the map + result := make(map[string]string) + + // Split the input string by comma to get key=value pairs + pairs := strings.Split(input, ",") + + // Iterate over each pair + for _, pair := range pairs { + // Split each pair by '=' to get the key and value + kv := strings.SplitN(pair, "=", 2) + if len(kv) == 2 { + key := kv[0] + value := kv[1] + // Add the key and value to the map + result[key] = value + } + } + + return result +} + // transportFromString converts a string to a transport type. // Defaults to http if the string is not recognized. func transportFromString(transport string) otlpTransportType { @@ -114,3 +281,8 @@ func transportFromString(transport string) otlpTransportType { // (see uber's go stye guide for details) return otlpTransportType(0) } + +const ( + defaultMaxQueueSize = 1000000 + defaultMaxExportBatch = 2000 +) diff --git a/core/metrics/otlp_test.go b/core/metrics/otlp_test.go new file mode 100644 index 0000000000..55a127836a --- /dev/null +++ b/core/metrics/otlp_test.go @@ -0,0 +1,71 @@ +package metrics_test + +import ( + "github.com/synapsecns/sanguine/core/metrics" + "reflect" + "testing" +) + +func TestHeadersToMap(t *testing.T) { + tests := []struct { + name string + input string + expected map[string]string + }{ + { + name: "basic input", + input: "key1=value1,key2=value2", + expected: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + { + name: "empty input", + input: "", + expected: map[string]string{}, + }, + { + name: "input with extra spaces", + input: "key1 = value1 , key2= value2 ", + expected: map[string]string{ + "key1 ": " value1 ", + " key2": " value2 ", + }, + }, + { + name: "input with missing value", + input: "key1=value1,key2=", + expected: map[string]string{ + "key1": "value1", + "key2": "", + }, + }, + { + name: "input with missing key", + input: "=value1,key2=value2", + expected: map[string]string{ + "": "value1", + "key2": "value2", + }, + }, + { + name: "input with multiple equal signs", + input: "key1=value1=extra,key2=value2", + expected: map[string]string{ + "key1": "value1=extra", + "key2": "value2", + }, + }, + } + + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + result := metrics.HeadersToMap(tt.input) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("HeadersToMap(%v) = %v; want %v", tt.input, result, tt.expected) + } + }) + } +} diff --git a/core/metrics/rookout.go b/core/metrics/rookout.go index 88b72a611d..5ed6302ccb 100644 --- a/core/metrics/rookout.go +++ b/core/metrics/rookout.go @@ -3,12 +3,13 @@ package metrics import ( + "os" + rookout "github.com/Rookout/GoSDK" "github.com/Rookout/GoSDK/pkg/config" "github.com/synapsecns/sanguine/core" synconfig "github.com/synapsecns/sanguine/core/config" "github.com/synapsecns/sanguine/core/metrics/internal" - "os" ) // DefaultGitRepo is the default git repo for sanguine. diff --git a/docs/bridge/docs/rfq/API/API.md b/docs/bridge/docs/rfq/API/API.md index 392f91e1c0..dfbc77ffe3 100644 --- a/docs/bridge/docs/rfq/API/API.md +++ b/docs/bridge/docs/rfq/API/API.md @@ -25,6 +25,16 @@ The RFQ API is a RESTful API that allows users to post quotes and read quotes. T Only Solvers should be writing to the API, end-users need only read from the `/quotes` endpoint. +**API Version Changes** + +An http response header "X-Api-Version" will be returned on each call response. + +Any systems that integrate with the API should use this header to detect version changes and perform appropriate follow-up actions & alerts. + +Upon a version change, [versions.go](https://github.com/synapsecns/sanguine/blob/master/services/rfq/api/rest/versions.go) can be referred to for further detail on the version including deprecation alerts, etc. + +Please note, while Synapse may choose to take additional steps to alert & advise on API changes through other communication channels, it will remain the responsibility of the API users & integrators to set up their own detection & notifications of version changes as they use these endpoints. Likewise, it will be their responsibility review the versions.go file, to research & understand how any changes may affect their integration, and to implement any necessary adjustments resulting from the API changes. + **Authentication** In accordance with [EIP-191](https://eips.ethereum.org/EIPS/eip-191), the RFQ API requires a signature to be sent with each request. The signature should be generated by the user's wallet and should be a valid signature of the message `rfq-api` with the user's private key. The signature should be sent in the `Authorization` header of the request. We provide a client stub/example implementation in go [here](https://pkg.go.dev/github.com/synapsecns/sanguine/services/rfq@v0.13.3/api/client). diff --git a/packages/contracts-rfq/CHANGELOG.md b/packages/contracts-rfq/CHANGELOG.md index e6bb7bcdb9..4c55dc04d6 100644 --- a/packages/contracts-rfq/CHANGELOG.md +++ b/packages/contracts-rfq/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.3.0](https://github.com/synapsecns/sanguine/compare/FastBridge@0.2.14...FastBridge@0.3.0) (2024-09-10) + + +### Features + +* **contracts-rfq:** Multicall target abstraction [SLT-134] ([#3078](https://github.com/synapsecns/sanguine/issues/3078)) ([100324f](https://github.com/synapsecns/sanguine/commit/100324f269f77f73fc10913d0162676f5f918996)) + + + + + ## [0.2.14](https://github.com/synapsecns/sanguine/compare/FastBridge@0.2.13...FastBridge@0.2.14) (2024-07-29) **Note:** Version bump only for package FastBridge diff --git a/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol new file mode 100644 index 0000000000..1f48e59609 --- /dev/null +++ b/packages/contracts-rfq/contracts/interfaces/IMulticallTarget.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @notice Interface for a contract that can be called multiple times by the same caller. Inspired by MulticallV3: +/// https://github.com/mds1/multicall/blob/master/src/Multicall3.sol +interface IMulticallTarget { + struct Result { + bool success; + bytes returnData; + } + + function multicallNoResults(bytes[] calldata data, bool ignoreReverts) external; + function multicallWithResults( + bytes[] calldata data, + bool ignoreReverts + ) + external + returns (Result[] memory results); +} diff --git a/packages/contracts-rfq/contracts/utils/MulticallTarget.sol b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol new file mode 100644 index 0000000000..bed9266c33 --- /dev/null +++ b/packages/contracts-rfq/contracts/utils/MulticallTarget.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IMulticallTarget} from "../interfaces/IMulticallTarget.sol"; + +// solhint-disable avoid-low-level-calls +/// @notice Template for a contract that supports batched calls (preserving the msg.sender). +/// Only calls with zero msg.value could be batched. +abstract contract MulticallTarget is IMulticallTarget { + error MulticallTarget__UndeterminedRevert(); + + /// @notice Perform a batched call to this contract, preserving the msg.sender. + /// The return data from each call is discarded. + /// @dev The method is non-payable, so only calls with `msg.value == 0` could be batched. + /// It's possible to ignore the reverts from the calls by setting the `ignoreReverts` flag. + /// Otherwise, the whole batch call will be reverted with the original revert reason. + /// @param data List of abi-encoded calldata for the calls to perform. + /// @param ignoreReverts Whether to ignore the revert errors from the calls. + function multicallNoResults(bytes[] calldata data, bool ignoreReverts) external { + for (uint256 i = 0; i < data.length; ++i) { + // We perform a delegate call to ourself to preserve the msg.sender. This is identical to `msg.sender` + // calling the functions directly one by one, therefore doesn't add any security risks. + // Note: msg.value is also preserved when doing a delegate call, but this function is not payable, + // so it's always 0 and not a security risk. + (bool success, bytes memory result) = address(this).delegatecall(data[i]); + if (!success && !ignoreReverts) { + _bubbleRevert(result); + } + } + } + + /// @notice Perform a batched call to this contract, preserving the msg.sender. + /// The return data from each call is preserved. + /// @dev The method is non-payable, so only calls with `msg.value == 0` could be batched. + /// It's possible to ignore the reverts from the calls by setting the `ignoreReverts` flag. + /// Otherwise, the whole batch call will be reverted with the original revert reason. + /// @param data List of abi-encoded calldata for the calls to perform. + /// @param ignoreReverts Whether to ignore the revert errors from the calls. + /// @return results List of results from the calls: `(success, returnData)`. + function multicallWithResults( + bytes[] calldata data, + bool ignoreReverts + ) + external + returns (Result[] memory results) + { + results = new Result[](data.length); + for (uint256 i = 0; i < data.length; ++i) { + // We perform a delegate call to ourself to preserve the msg.sender. This is identical to `msg.sender` + // calling the functions directly one by one, therefore doesn't add any security risks. + // Note: msg.value is also preserved when doing a delegate call, but this function is not payable, + // so it's always 0 and not a security risk. + (results[i].success, results[i].returnData) = address(this).delegatecall(data[i]); + if (!results[i].success && !ignoreReverts) { + _bubbleRevert(results[i].returnData); + } + } + } + + /// @dev Bubbles the revert message from the underlying call. + /// Note: preserves the same custom error or revert string, if one was used. + /// Source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/Address.sol#L143-L158 + function _bubbleRevert(bytes memory returnData) internal pure { + // Look for revert reason and bubble it up if present + if (returnData.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returnData) + revert(add(32, returnData), returndata_size) + } + } else { + revert MulticallTarget__UndeterminedRevert(); + } + } +} diff --git a/packages/contracts-rfq/package.json b/packages/contracts-rfq/package.json index 15c7279950..d778482a30 100644 --- a/packages/contracts-rfq/package.json +++ b/packages/contracts-rfq/package.json @@ -1,7 +1,7 @@ { "name": "FastBridge", "license": "UNLICENSED", - "version": "0.2.14", + "version": "0.3.0", "description": "FastBridge contracts.", "private": true, "files": [ diff --git a/packages/contracts-rfq/test/MulticallTarget.t.sol b/packages/contracts-rfq/test/MulticallTarget.t.sol new file mode 100644 index 0000000000..1c12bcaca5 --- /dev/null +++ b/packages/contracts-rfq/test/MulticallTarget.t.sol @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IMulticallTarget} from "../contracts/interfaces/IMulticallTarget.sol"; +import {MulticallTargetHarness, MulticallTarget} from "./harnesses/MulticallTargetHarness.sol"; + +import {Test} from "forge-std/Test.sol"; + +// solhint-disable func-name-mixedcase, ordering +contract MulticallTargetTest is Test { + MulticallTargetHarness public harness; + + address public caller = makeAddr("Caller"); + + function setUp() public { + harness = new MulticallTargetHarness(); + harness.setAddressField(address(1)); + harness.setUintField(2); + } + + function getEncodedStringRevertMessage() internal view returns (bytes memory) { + return abi.encodeWithSignature("Error(string)", harness.REVERT_MESSAGE()); + } + + function getMsgSenderData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.addressField, ()), + abi.encodeCall(harness.setMsgSenderAsAddressField, ()), + abi.encodeCall(harness.addressField, ()) + ); + } + + function getMsgSenderResults() internal view returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(caller)), + IMulticallTarget.Result(true, abi.encode(caller)) + ); + } + + function getNoRevertsData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.addressField, ()), + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.setUintField, (42)), + abi.encodeCall(harness.setAddressField, (address(0xDEADBEAF))) + ); + } + + function getNoRevertsResults() internal pure returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1))), + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(42)), + IMulticallTarget.Result(true, abi.encode(address(0xDEADBEAF))) + ); + } + + function getCustomErrorRevertData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.setUintField, (42)), + abi.encodeCall(harness.customErrorRevert, ()), + abi.encodeCall(harness.setAddressField, (address(0xDEADBEAF))) + ); + } + + function getCustomErrorRevertResults() internal pure returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(42)), + IMulticallTarget.Result(false, abi.encodeWithSelector(MulticallTargetHarness.CustomError.selector)), + IMulticallTarget.Result(true, abi.encode(address(0xDEADBEAF))) + ); + } + + function getStringRevertData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.setUintField, (42)), + abi.encodeCall(harness.revertingFunction, ()), + abi.encodeCall(harness.setAddressField, (address(0xDEADBEAF))) + ); + } + + function getStringRevertResults() internal view returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(42)), + IMulticallTarget.Result(false, abi.encodeWithSignature("Error(string)", harness.REVERT_MESSAGE())), + IMulticallTarget.Result(true, abi.encode(address(0xDEADBEAF))) + ); + } + + function getUndeterminedRevertData() internal view returns (bytes[] memory) { + return toArray( + abi.encodeCall(harness.setAddressField, (address(1234))), + abi.encodeCall(harness.setUintField, (42)), + abi.encodeCall(harness.undeterminedRevert, ()), + abi.encodeCall(harness.setAddressField, (address(0xDEADBEAF))) + ); + } + + function getUndeterminedRevertResults() internal pure returns (IMulticallTarget.Result[] memory) { + return toArray( + IMulticallTarget.Result(true, abi.encode(address(1234))), + IMulticallTarget.Result(true, abi.encode(42)), + IMulticallTarget.Result(false, ""), + IMulticallTarget.Result(true, abi.encode(address(0xDEADBEAF))) + ); + } + + // ══════════════════════════════════════════ MULTICALL (NO RESULTS) ═══════════════════════════════════════════════ + + function test_multicallNoResults_ignoreReverts_noReverts() public { + bytes[] memory data = getNoRevertsData(); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_ignoreReverts_withMsgSender() public { + bytes[] memory data = getMsgSenderData(); + vm.prank(caller); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), caller); + assertEq(harness.uintField(), 2); + } + + function test_multicallNoResults_ignoreReverts_withCustomErrorRevert() public { + bytes[] memory data = getCustomErrorRevertData(); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_ignoreReverts_withStringRevert() public { + bytes[] memory data = getStringRevertData(); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_ignoreReverts_withUndeterminedRevert() public { + bytes[] memory data = getUndeterminedRevertData(); + harness.multicallNoResults({data: data, ignoreReverts: true}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_dontIgnoreReverts_noReverts() public { + bytes[] memory data = getNoRevertsData(); + harness.multicallNoResults({data: data, ignoreReverts: false}); + + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallNoResults_dontIgnoreReverts_withMsgSender() public { + bytes[] memory data = getMsgSenderData(); + vm.prank(caller); + harness.multicallNoResults({data: data, ignoreReverts: false}); + + assertEq(harness.addressField(), caller); + assertEq(harness.uintField(), 2); + } + + function test_multicallNoResults_dontIgnoreReverts_withCustomErrorRevert() public { + bytes[] memory data = getCustomErrorRevertData(); + vm.expectRevert(MulticallTargetHarness.CustomError.selector); + harness.multicallNoResults({data: data, ignoreReverts: false}); + } + + function test_multicallNoResults_dontIgnoreReverts_withStringRevert() public { + bytes[] memory data = getStringRevertData(); + string memory revertMessage = harness.REVERT_MESSAGE(); + vm.expectRevert(bytes(revertMessage)); + harness.multicallNoResults({data: data, ignoreReverts: false}); + } + + function test_multicallNoResults_dontIgnoreReverts_withUndeterminedRevert() public { + bytes[] memory data = getUndeterminedRevertData(); + vm.expectRevert(MulticallTarget.MulticallTarget__UndeterminedRevert.selector); + harness.multicallNoResults({data: data, ignoreReverts: false}); + } + + // ═════════════════════════════════════════ MULTICALL (WITH RESULTS) ══════════════════════════════════════════════ + + function test_multicallWithResults_ignoreReverts_noReverts() public { + bytes[] memory data = getNoRevertsData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getNoRevertsResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_ignoreReverts_withMsgSender() public { + bytes[] memory data = getMsgSenderData(); + vm.prank(caller); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getMsgSenderResults()); + assertEq(harness.uintField(), 2); + assertEq(harness.addressField(), caller); + } + + function test_multicallWithResults_ignoreReverts_withCustomErrorRevert() public { + bytes[] memory data = getCustomErrorRevertData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getCustomErrorRevertResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_ignoreReverts_withStringRevert() public { + bytes[] memory data = getStringRevertData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getStringRevertResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_ignoreReverts_withUndeterminedRevert() public { + bytes[] memory data = getUndeterminedRevertData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: true}); + + assertEq(results, getUndeterminedRevertResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_dontIgnoreReverts_noReverts() public { + bytes[] memory data = getNoRevertsData(); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: false}); + + assertEq(results, getNoRevertsResults()); + assertEq(harness.addressField(), address(0xDEADBEAF)); + assertEq(harness.uintField(), 42); + } + + function test_multicallWithResults_dontIgnoreReverts_withMsgSender() public { + bytes[] memory data = getMsgSenderData(); + vm.prank(caller); + IMulticallTarget.Result[] memory results = harness.multicallWithResults({data: data, ignoreReverts: false}); + + assertEq(results, getMsgSenderResults()); + assertEq(harness.addressField(), caller); + assertEq(harness.uintField(), 2); + } + + function test_multicallWithResults_dontIgnoreReverts_withCustomErrorRevert() public { + bytes[] memory data = getCustomErrorRevertData(); + vm.expectRevert(MulticallTargetHarness.CustomError.selector); + harness.multicallWithResults({data: data, ignoreReverts: false}); + } + + function test_multicallWithResults_dontIgnoreReverts_withStringRevert() public { + bytes[] memory data = getStringRevertData(); + string memory revertMessage = harness.REVERT_MESSAGE(); + vm.expectRevert(bytes(revertMessage)); + harness.multicallWithResults({data: data, ignoreReverts: false}); + } + + function test_multicallWithResults_dontIgnoreReverts_withUndeterminedRevert() public { + bytes[] memory data = getUndeterminedRevertData(); + vm.expectRevert(MulticallTarget.MulticallTarget__UndeterminedRevert.selector); + harness.multicallWithResults({data: data, ignoreReverts: false}); + } + + // ══════════════════════════════════════════════════ VIEW ════════════════════════════════════════════════════ + + function assertEq(IMulticallTarget.Result memory a, IMulticallTarget.Result memory b) internal pure { + assertEq(a.success, b.success); + assertEq(a.returnData, b.returnData); + } + + function assertEq(IMulticallTarget.Result[] memory a, IMulticallTarget.Result[] memory b) internal pure { + assertEq(a.length, b.length); + for (uint256 i = 0; i < a.length; i++) { + assertEq(a[i], b[i]); + } + } + + function toArray( + bytes memory a, + bytes memory b, + bytes memory c, + bytes memory d + ) + internal + pure + returns (bytes[] memory arr) + { + arr = new bytes[](4); + arr[0] = a; + arr[1] = b; + arr[2] = c; + arr[3] = d; + } + + function toArray( + IMulticallTarget.Result memory a, + IMulticallTarget.Result memory b, + IMulticallTarget.Result memory c, + IMulticallTarget.Result memory d + ) + internal + pure + returns (IMulticallTarget.Result[] memory arr) + { + arr = new IMulticallTarget.Result[](4); + arr[0] = a; + arr[1] = b; + arr[2] = c; + arr[3] = d; + } +} diff --git a/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol b/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol new file mode 100644 index 0000000000..5819dbf3fc --- /dev/null +++ b/packages/contracts-rfq/test/harnesses/MulticallTargetHarness.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {MulticallTarget} from "../../contracts/utils/MulticallTarget.sol"; + +contract MulticallTargetHarness is MulticallTarget { + address public addressField; + uint256 public uintField; + + string public constant REVERT_MESSAGE = "gm, this is a revert message"; + + error CustomError(); + + function setMsgSenderAsAddressField() external returns (address) { + addressField = msg.sender; + return addressField; + } + + function setAddressField(address _addressField) external returns (address) { + addressField = _addressField; + return addressField; + } + + function setUintField(uint256 _uintField) external returns (uint256) { + uintField = _uintField; + return uintField; + } + + function customErrorRevert() external pure { + revert CustomError(); + } + + function revertingFunction() external pure { + revert(REVERT_MESSAGE); + } + + function undeterminedRevert() external pure { + // solhint-disable-next-line no-inline-assembly + assembly { + revert(0, 0) + } + } +} diff --git a/packages/rest-api/CHANGELOG.md b/packages/rest-api/CHANGELOG.md index 1665e37772..289c876e32 100644 --- a/packages/rest-api/CHANGELOG.md +++ b/packages/rest-api/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.73](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.0.72...@synapsecns/rest-api@1.0.73) (2024-09-12) + +**Note:** Version bump only for package @synapsecns/rest-api + + + + + ## [1.0.72](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.0.71...@synapsecns/rest-api@1.0.72) (2024-09-04) diff --git a/packages/rest-api/package.json b/packages/rest-api/package.json index ccc3784f7b..df37861794 100644 --- a/packages/rest-api/package.json +++ b/packages/rest-api/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/rest-api", - "version": "1.0.72", + "version": "1.0.73", "private": "true", "engines": { "node": ">=18.17.0" diff --git a/packages/rest-api/src/app.ts b/packages/rest-api/src/app.ts index 104db349ff..e9f236f8b1 100644 --- a/packages/rest-api/src/app.ts +++ b/packages/rest-api/src/app.ts @@ -471,6 +471,182 @@ app.get('/bridgeTxInfo', async (req, res) => { } }) +// Get Synapse Transaction ID +app.get('/getSynapseTxId', (req, res) => { + try { + const query = req.query + const originChainId = Number(query.originChainId) + const bridgeModule = String(query.bridgeModule) + const txHash = String(query.txHash) + + if (!originChainId || !bridgeModule || !txHash) { + res.status(400).send({ + message: 'Invalid request: Missing required parameters', + }) + return + } + + Synapse.getSynapseTxId(originChainId, bridgeModule, txHash) + .then((synapseTxId) => { + res.json({ synapseTxId }) + }) + .catch((err) => { + res.status(400).send({ + message: + 'Ensure that your request matches the following format: /getSynapseTxId?originChainId=8453&bridgeModule=SynapseRFQ&txHash=0x4acd82091b54cf584d50adcad9f57c61055beaca130016ecc3798d3d61f5487a', + error: err.message, + }) + }) + } catch (err) { + res.status(400).send({ + message: + 'Ensure that your request matches the following format: /getSynapseTxId?originChainId=8453&bridgeModule=SynapseRFQ&txHash=0x4acd82091b54cf584d50adcad9f57c61055beaca130016ecc3798d3d61f5487a', + error: err.message, + }) + } +}) + +// Get Bridge Transaction Status +app.get('/getBridgeTxStatus', async (req, res) => { + try { + const query = req.query + const destChainId = Number(query.destChainId) + const bridgeModule = String(query.bridgeModule) + const synapseTxId = String(query.synapseTxId) + + if (!destChainId || !bridgeModule || !synapseTxId) { + res.status(400).send({ + message: 'Invalid request: Missing required parameters', + }) + return + } + + try { + const status = await Synapse.getBridgeTxStatus( + destChainId, + bridgeModule, + synapseTxId + ) + + if (status) { + const txIdWithout0x = synapseTxId.startsWith('0x') + ? synapseTxId.slice(2) + : synapseTxId + const graphqlEndpoint = 'https://explorer.omnirpc.io/graphql' + const graphqlQuery = ` + { + bridgeTransactions( + useMv: true + kappa: "${txIdWithout0x}" + ) { + toInfo { + chainID + address + txnHash + USDValue + tokenSymbol + blockNumber + formattedTime + } + } + } + ` + + const graphqlResponse = await fetch(graphqlEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query: graphqlQuery }), + }) + + const graphqlData = await graphqlResponse.json() + const toInfo = graphqlData.data.bridgeTransactions[0]?.toInfo || null + + res.json({ status, toInfo }) + } else { + res.json({ status }) + } + } catch (err) { + res.status(400).send({ + message: 'Error fetching bridge transaction status', + error: err.message, + }) + } + } catch (err) { + res.status(400).send({ + message: 'Invalid request', + error: err.message, + }) + } +}) + +// Get Destination Transaction Hash +app.get('/getDestinationTx', async (req, res) => { + try { + const query = req.query + const originChainId = Number(query.originChainId) + const txHash = String(query.txHash) + + if (!originChainId || !txHash) { + res.status(400).send({ + message: 'Invalid request: Missing required parameters', + }) + return + } + + try { + const graphqlEndpoint = 'https://explorer.omnirpc.io/graphql' + const graphqlQuery = ` + { + bridgeTransactions( + useMv: true + chainIDFrom: ${originChainId} + txnHash: "${txHash}" + ) { + toInfo { + chainID + address + txnHash + USDValue + tokenSymbol + blockNumber + formattedTime + } + } + } + ` + + const graphqlResponse = await fetch(graphqlEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query: graphqlQuery }), + }) + + const graphqlData = await graphqlResponse.json() + const toInfo = graphqlData.data.bridgeTransactions[0]?.toInfo || null + + if (toInfo === null) { + res.json({ status: 'pending' }) + } else { + res.json({ status: 'completed', toInfo }) + } + } catch (err) { + res.status(400).send({ + message: 'Error fetching bridge transaction status', + error: err.message, + }) + } + } catch (err) { + res.status(400).send({ + message: 'Invalid request', + error: err.message, + }) + } +}) + export const server = app.listen(port, () => { console.log(`Server listening at ${port}`) }) diff --git a/packages/synapse-interface/CHANGELOG.md b/packages/synapse-interface/CHANGELOG.md index d89d22a7a5..6e75c970f7 100644 --- a/packages/synapse-interface/CHANGELOG.md +++ b/packages/synapse-interface/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.38.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.2...@synapsecns/synapse-interface@0.38.3) (2024-09-10) + +**Note:** Version bump only for package @synapsecns/synapse-interface + + + + + ## [0.38.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.1...@synapsecns/synapse-interface@0.38.2) (2024-09-10) **Note:** Version bump only for package @synapsecns/synapse-interface diff --git a/packages/synapse-interface/components/LanguageSelector.tsx b/packages/synapse-interface/components/LanguageSelector.tsx index 2b51d5f920..c9236ee246 100644 --- a/packages/synapse-interface/components/LanguageSelector.tsx +++ b/packages/synapse-interface/components/LanguageSelector.tsx @@ -12,6 +12,7 @@ const languages = [ { code: 'es', name: 'Español' }, { code: 'fr', name: 'Français' }, { code: 'tr', name: 'Türkçe' }, + { code: 'zh-CN', name: '中文(简体)' }, ] export const LanguageSelector = () => { diff --git a/packages/synapse-interface/next.config.js b/packages/synapse-interface/next.config.js index 5e59674626..2b7541ac02 100644 --- a/packages/synapse-interface/next.config.js +++ b/packages/synapse-interface/next.config.js @@ -44,7 +44,7 @@ const nextConfig = { tsconfigPath: './tsconfig.json', }, i18n: { - locales: ['en-US', 'fr', 'ar', 'tr', 'es'], + locales: ['en-US', 'fr', 'ar', 'tr', 'es', 'zh-CN'], defaultLocale: 'en-US', }, } diff --git a/packages/synapse-interface/package.json b/packages/synapse-interface/package.json index 006659796b..3c6c2abaf4 100644 --- a/packages/synapse-interface/package.json +++ b/packages/synapse-interface/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/synapse-interface", - "version": "0.38.2", + "version": "0.38.3", "private": true, "engines": { "node": ">=18.18.0" diff --git a/packages/synapse-interface/public/blacklist.json b/packages/synapse-interface/public/blacklist.json index 6d854278c5..99fd9dba86 100644 --- a/packages/synapse-interface/public/blacklist.json +++ b/packages/synapse-interface/public/blacklist.json @@ -534,5 +534,6 @@ "0x8e80e055e2790c9dFEeCa5D9ce11a485e70eB244", "0xfdc0dfbcbe0afad28827dfa216b9adb9cdb90dfd", "0xE484E76932fF72dD3B0867B9dC4C0690E081C2F5", - "0x1Da02f048F0b4Fc57169f1958f996b317518029a" + "0x1Da02f048F0b4Fc57169f1958f996b317518029a", + "0xB0A2e43D3E0dc4C71346A71484aC6a2627bbCbeD" ] diff --git a/services/rfq/api/docs/docs.go b/services/rfq/api/docs/docs.go index c68a9eb52c..af06bfa449 100644 --- a/services/rfq/api/docs/docs.go +++ b/services/rfq/api/docs/docs.go @@ -41,7 +41,13 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } + } } } } @@ -72,7 +78,13 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } + } } } } @@ -98,6 +110,12 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/model.GetContractsResponse" } + }, + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } } } } @@ -156,6 +174,12 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/model.GetQuoteResponse" } + }, + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } } } } @@ -185,7 +209,13 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } + } } } } diff --git a/services/rfq/api/docs/swagger.json b/services/rfq/api/docs/swagger.json index f6bdd6e25b..5d28bbf785 100644 --- a/services/rfq/api/docs/swagger.json +++ b/services/rfq/api/docs/swagger.json @@ -30,7 +30,13 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } + } } } } @@ -61,7 +67,13 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } + } } } } @@ -87,6 +99,12 @@ "items": { "$ref": "#/definitions/model.GetContractsResponse" } + }, + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } } } } @@ -145,6 +163,12 @@ "items": { "$ref": "#/definitions/model.GetQuoteResponse" } + }, + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } } } } @@ -174,7 +198,13 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "headers": { + "X-Api-Version": { + "type": "string", + "description": "API Version Number - See docs for more info" + } + } } } } diff --git a/services/rfq/api/docs/swagger.yaml b/services/rfq/api/docs/swagger.yaml index c39cace1ef..a8ddffdcc6 100644 --- a/services/rfq/api/docs/swagger.yaml +++ b/services/rfq/api/docs/swagger.yaml @@ -99,6 +99,10 @@ paths: responses: "200": description: OK + headers: + X-Api-Version: + description: API Version Number - See docs for more info + type: string summary: Relay ack tags: - ack @@ -119,6 +123,10 @@ paths: responses: "200": description: OK + headers: + X-Api-Version: + description: API Version Number - See docs for more info + type: string summary: Upsert quotes tags: - quotes @@ -132,6 +140,10 @@ paths: responses: "200": description: OK + headers: + X-Api-Version: + description: API Version Number - See docs for more info + type: string schema: items: $ref: '#/definitions/model.GetContractsResponse' @@ -170,6 +182,10 @@ paths: responses: "200": description: OK + headers: + X-Api-Version: + description: API Version Number - See docs for more info + type: string schema: items: $ref: '#/definitions/model.GetQuoteResponse' @@ -193,6 +209,10 @@ paths: responses: "200": description: OK + headers: + X-Api-Version: + description: API Version Number - See docs for more info + type: string summary: Upsert quote tags: - quotes diff --git a/services/rfq/api/rest/handler.go b/services/rfq/api/rest/handler.go index 4e2f61b802..2878fb4cb3 100644 --- a/services/rfq/api/rest/handler.go +++ b/services/rfq/api/rest/handler.go @@ -49,6 +49,7 @@ func APIVersionMiddleware(serverVersion string) gin.HandlerFunc { // @Accept json // @Produce json // @Success 200 +// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info" // @Router /quotes [put]. func (h *Handler) ModifyQuote(c *gin.Context) { // Retrieve the request from context @@ -94,6 +95,7 @@ func (h *Handler) ModifyQuote(c *gin.Context) { // @Accept json // @Produce json // @Success 200 +// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info" // @Router /bulk_quotes [put]. func (h *Handler) ModifyBulkQuotes(c *gin.Context) { // Retrieve the request from context @@ -176,6 +178,7 @@ func parseDBQuote(putRequest model.PutQuoteRequest, relayerAddr interface{}) (*d // @Accept json // @Produce json // @Success 200 {array} model.GetQuoteResponse +// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info" // @Router /quotes [get]. func (h *Handler) GetQuotes(c *gin.Context) { originChainIDStr := c.Query("originChainID") @@ -240,6 +243,7 @@ func (h *Handler) GetQuotes(c *gin.Context) { // @Accept json // @Produce json // @Success 200 {array} model.GetContractsResponse +// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info" // @Router /contracts [get]. func (h *Handler) GetContracts(c *gin.Context) { // Convert quotes from db model to api model diff --git a/services/rfq/api/rest/server.go b/services/rfq/api/rest/server.go index 3c961b65f3..a3a3b32a6f 100644 --- a/services/rfq/api/rest/server.go +++ b/services/rfq/api/rest/server.go @@ -330,6 +330,7 @@ func (r *QuoterAPIServer) checkRole(c *gin.Context, destChainID uint32) (address // @Accept json // @Produce json // @Success 200 +// @Header 200 {string} X-Api-Version "API Version Number - See docs for more info" // @Router /ack [put]. func (r *QuoterAPIServer) PutRelayAck(c *gin.Context) { req, exists := c.Get("putRequest")