Repository: stellar/stellar-disbursement-platform-backend
Pull request #663: SDP-1611: Upgrade to AP 3.2.0-beta.1 and set up config
Author: login: philipliu
Created: 2025-05-02T21:14:11Z | Merged: 2025-05-05T20:55:43Z

### What

This PR upgrades the Anchor Platform to `3.2.0-beta.1`, which includes support for SEP-45. When `--sep45-contract-id` and `--rpc-url` are set in the Docker Compose, SEP-45 will be enabled in Anchor Platform, and the `stellar.toml` will include the `WEB_AUTH_CONTRACT_ID` and `WEB_AUTH_FOR_CONTRACTS_ENDPOINT` fields. Helm charts will need to be updated to do the same in a future PR.

This change is backward compatible, meaning if `--sep45-contract-id` is not set, then `--rpc-url` is not required.

### Why

Minimum changes to enable SEP-45 and future RPC integration in SDP.

### Known limitations

Instructions for deploying the SEP-45 contract are not provided, and none of the deployment scripts have been updated. It's likely that SDF will deploy a reference contract implementation, and the code will point to that. 

### Checklist

- [x] Title follows `SDP-1234: Add new feature` or `Chore: Refactor package xyz` format. The Jira ticket code was included if available.
- [x] PR has a focused scope and doesn't mix features with refactoring
- [x] Tests are included (if applicable)
- [ ] `CHANGELOG.md` is updated (if applicable)
- [ ] CONFIG/SECRETS changes are updated in helmcharts and deployments (if applicable)
- [ ] Preview deployment works as expected
- [ ] Ready for production



Diff:
diff --git a/.env.example b/.env.example
index 88c12f534..d70d32631 100644
--- a/.env.example
+++ b/.env.example
@@ -19,6 +19,9 @@ ANCHOR_PLATFORM_BASE_PLATFORM_URL=http://localhost:8085
 ANCHOR_PLATFORM_BASE_SEP_URL=http://localhost:8080
 ANCHOR_PLATFORM_BASE_URL=http://localhost:8090
 
+# Deploy a SEP-45 web auth contract to enable SEP-45 authentication
+SEP45_CONTRACT_ID=
+
 CORS_ALLOWED_ORIGINS=*
 BASE_URL=http://stellar.local:8000
 SDP_UI_BASE_URL=http://stellar.local:3000
diff --git a/cmd/serve.go b/cmd/serve.go
index 2e3e1923b..07860892d 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -281,6 +281,14 @@ func (c *ServeCommand) Command(serverService ServerServiceInterface, monitorServ
 			ConfigKey:      &serveOpts.Sep10SigningPrivateKey,
 			Required:       true,
 		},
+		{
+			Name:           "sep45-contract-id",
+			Usage:          "The ID of the SEP-45 web authentication contract",
+			OptType:        types.String,
+			CustomSetValue: cmdUtils.SetConfigOptionStellarContractId,
+			ConfigKey:      &serveOpts.Sep45ContractId,
+			Required:       false,
+		},
 		{
 			Name: "anchor-platform-base-platform-url",
 			Usage: "The Base URL of the platform server of the anchor platform. This is the base URL where the Anchor Platform " +
@@ -375,6 +383,8 @@ func (c *ServeCommand) Command(serverService ServerServiceInterface, monitorServ
 			FlagDefault:    string(circle.APITypeTransfers),
 		},
 	}
+	// rpc options
+	configOpts = append(configOpts, cmdUtils.RpcConfigOptions(&serveOpts.RpcConfig)...)
 
 	// crash tracker options
 	crashTrackerOptions := crashtracker.CrashTrackerOptions{}
diff --git a/cmd/serve_test.go b/cmd/serve_test.go
index e52a71161..2963e0373 100644
--- a/cmd/serve_test.go
+++ b/cmd/serve_test.go
@@ -30,6 +30,7 @@ import (
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/serve"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httpclient"
 	svcMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/services/mocks"
+	"github.com/stellar/stellar-disbursement-platform-backend/internal/stellar"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine"
 	preconditionsMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/preconditions/mocks"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/signing"
@@ -164,6 +165,8 @@ func Test_serve(t *testing.T) {
 		NetworkType:                     utils.TestnetNetworkType,
 		Sep10SigningPublicKey:           "GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S",
 		Sep10SigningPrivateKey:          "SBUSPEKAZKLZSWHRSJ2HWDZUK6I3IVDUWA7JJZSGBLZ2WZIUJI7FPNB5",
+		Sep45ContractId:                 "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX",
+		RpcConfig:                       stellar.RpcOptions{RpcURL: "https://soroban-testnet.stellar.org"},
 		AnchorPlatformBaseSepURL:        "localhost:8080",
 		AnchorPlatformBasePlatformURL:   "localhost:8085",
 		AnchorPlatformOutgoingJWTSecret: "jwt_secret_1234567890",
@@ -295,6 +298,8 @@ func Test_serve(t *testing.T) {
 	t.Setenv("SEP24_JWT_SECRET", serveOpts.SEP24JWTSecret)
 	t.Setenv("SEP10_SIGNING_PUBLIC_KEY", serveOpts.Sep10SigningPublicKey)
 	t.Setenv("SEP10_SIGNING_PRIVATE_KEY", serveOpts.Sep10SigningPrivateKey)
+	t.Setenv("SEP45_CONTRACT_ID", serveOpts.Sep45ContractId)
+	t.Setenv("RPC_URL", serveOpts.RpcConfig.RpcURL)
 	t.Setenv("ANCHOR_PLATFORM_BASE_SEP_URL", serveOpts.AnchorPlatformBaseSepURL)
 	t.Setenv("ANCHOR_PLATFORM_BASE_PLATFORM_URL", serveOpts.AnchorPlatformBasePlatformURL)
 	t.Setenv("ANCHOR_PLATFORM_OUTGOING_JWT_SECRET", serveOpts.AnchorPlatformOutgoingJWTSecret)
diff --git a/cmd/utils/custom_set_value.go b/cmd/utils/custom_set_value.go
index 5dcd2625e..95239e314 100644
--- a/cmd/utils/custom_set_value.go
+++ b/cmd/utils/custom_set_value.go
@@ -181,14 +181,45 @@ func SetConfigOptionStellarPrivateKey(co *config.ConfigOption) error {
 	return nil
 }
 
+func SetConfigOptionStellarContractId(co *config.ConfigOption) error {
+	contractId := viper.GetString(co.Name)
+	contractId = strings.TrimSpace(contractId)
+
+	if !co.Required && contractId == "" {
+		return nil
+	}
+
+	if contractId == "" {
+		return fmt.Errorf("contract id cannot be empty in %s", co.Name)
+	}
+
+	isValid := strkey.IsValidContractAddress(contractId)
+	if !isValid {
+		return fmt.Errorf("validating contract id in %s: %q", co.Name, contractId)
+	}
+
+	key, ok := co.ConfigKey.(*string)
+	if !ok {
+		return fmt.Errorf("the expected type for the config key in %s is a string, but a %T was provided instead", co.Name, co.ConfigKey)
+	}
+	*key = contractId
+
+	return nil
+}
+
 func SetConfigOptionURLString(co *config.ConfigOption) error {
-	u := viper.GetString(co.Name)
+	urlStr := viper.GetString(co.Name)
+	urlStr = strings.TrimSpace(urlStr)
+
+	if !co.Required && urlStr == "" {
+		return nil
+	}
 
-	if u == "" {
+	if urlStr == "" {
 		return fmt.Errorf("URL cannot be empty in %s", co.Name)
 	}
 
-	_, err := url.ParseRequestURI(u)
+	_, err := url.ParseRequestURI(urlStr)
 	if err != nil {
 		return fmt.Errorf("parsing URL in %s: %w", co.Name, err)
 	}
@@ -197,7 +228,7 @@ func SetConfigOptionURLString(co *config.ConfigOption) error {
 	if !ok {
 		return fmt.Errorf("the expected type for the config key in %s is a string, but a %T was provided instead", co.Name, co.ConfigKey)
 	}
-	*key = u
+	*key = urlStr
 
 	return nil
 }
diff --git a/cmd/utils/custom_set_value_test.go b/cmd/utils/custom_set_value_test.go
index 50c9881cd..7769de5a4 100644
--- a/cmd/utils/custom_set_value_test.go
+++ b/cmd/utils/custom_set_value_test.go
@@ -427,6 +427,68 @@ func Test_SetConfigOptionStellarPrivateKey(t *testing.T) {
 	}
 }
 
+func Test_SetConfigOptionStellarContractId(t *testing.T) {
+	opts := struct{ stellarContractId string }{}
+
+	co := config.ConfigOption{
+		Name:           "stellar-contract-id",
+		OptType:        types.String,
+		CustomSetValue: SetConfigOptionStellarContractId,
+		ConfigKey:      &opts.stellarContractId,
+		Required:       false,
+	}
+	expectedContractId := "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX"
+
+	testCases := []customSetterTestCase[string]{
+		{
+			name: "doesn't return an error if the contract id is empty",
+		},
+		{
+			name:            "returns an error if the contract id is invalid",
+			args:            []string{"--stellar-contract-id", "invalid_contract_id"},
+			wantErrContains: `validating contract id in stellar-contract-id: "invalid_contract_id"`,
+		},
+		{
+			name:            "returns an error if the contract id is invalid (public key instead)",
+			args:            []string{"--stellar-contract-id", "GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"},
+			wantErrContains: `validating contract id in stellar-contract-id: "GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"`,
+		},
+		{
+			name:            "returns an error if the contract id is invalid (private key instead)",
+			args:            []string{"--stellar-contract-id", "SBUSPEKAZKLZSWHRSJ2HWDZUK6I3IVDUWA7JJZSGBLZ2WZIUJI7FPNB5"},
+			wantErrContains: `validating contract id in stellar-contract-id: "SBUSPEKAZKLZSWHRSJ2HWDZUK6I3IVDUWA7JJZSGBLZ2WZIUJI7FPNB5"`,
+		},
+		{
+			name:       "🎉 handles Stellar contract id through the CLI flag",
+			args:       []string{"--stellar-contract-id", "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX"},
+			wantResult: expectedContractId,
+		},
+		{
+			name:       "🎉 handles Stellar contract id through the ENV flag",
+			envValue:   "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX",
+			wantResult: expectedContractId,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			opts.stellarContractId = ""
+			customSetterTester(t, tc, co)
+		})
+	}
+
+	tc := customSetterTestCase[[]string]{
+		name:            "returns an error if contract id is empty and it's required",
+		args:            []string{"--stellar-contract-id", "   "}, // Workaround to test empty values
+		wantErrContains: "cannot be empty",
+	}
+	t.Run(tc.name, func(t *testing.T) {
+		opts.stellarContractId = ""
+		co.Required = true
+		customSetterTester(t, tc, co)
+	})
+}
+
 func Test_SetCorsAllowedOriginsFunc(t *testing.T) {
 	opts := struct{ corsAddressesFlag []string }{}
 
@@ -503,9 +565,9 @@ func Test_SetConfigOptionURLString(t *testing.T) {
 
 	testCases := []customSetterTestCase[string]{
 		{
-			name:            "returns an error if the ui base url flag is empty",
-			args:            []string{"--sdp-ui-base-url", ""},
-			wantErrContains: "URL cannot be empty in sdp-ui-base-url",
+			name:       "🎉 handles ui base url is empty but it's not required",
+			args:       []string{"--sdp-ui-base-url", ""},
+			wantResult: "",
 		},
 		{
 			name:       "🎉 handles ui base url successfully (from CLI args)",
@@ -526,9 +588,20 @@ func Test_SetConfigOptionURLString(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
 			opts.uiBaseURL = ""
-			customSetterTester[string](t, tc, co)
+			customSetterTester(t, tc, co)
 		})
 	}
+
+	tc := customSetterTestCase[[]string]{
+		name:            "returns an error if ui base url is empty and it's required",
+		args:            []string{"--sdp-ui-base-url", "   "}, // Workaround to test empty values
+		wantErrContains: "cannot be empty",
+	}
+	t.Run(tc.name, func(t *testing.T) {
+		opts.uiBaseURL = ""
+		co.Required = true
+		customSetterTester(t, tc, co)
+	})
 }
 
 func Test_SetConfigOptionURLList(t *testing.T) {
diff --git a/cmd/utils/shared_config_options.go b/cmd/utils/shared_config_options.go
index c64f12280..5b0e95d85 100644
--- a/cmd/utils/shared_config_options.go
+++ b/cmd/utils/shared_config_options.go
@@ -14,6 +14,7 @@ import (
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/events"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/message"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/scheduler"
+	"github.com/stellar/stellar-disbursement-platform-backend/internal/stellar"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/signing"
 	"github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant"
 )
@@ -348,6 +349,33 @@ func HorizonURL(targetPointer interface{}) *config.ConfigOption {
 	}
 }
 
+func RpcConfigOptions(opts *stellar.RpcOptions) []*config.ConfigOption {
+	return []*config.ConfigOption{
+		{
+			Name:           "rpc-url",
+			Usage:          "The URL of the Stellar RPC server where this application will communicate with.",
+			OptType:        types.String,
+			CustomSetValue: SetConfigOptionURLString,
+			ConfigKey:      &opts.RpcURL,
+			Required:       false,
+		},
+		{
+			Name:      "rpc-request-header-key",
+			Usage:     "The key of the request header to be used for authentication with the RPC server.",
+			OptType:   types.String,
+			ConfigKey: &opts.RpcRequestHeaderKey,
+			Required:  false,
+		},
+		{
+			Name:      "rpc-request-header-value",
+			Usage:     "The value of the request header to be used for authentication with the RPC server.",
+			OptType:   types.String,
+			ConfigKey: &opts.RpcRequestHeaderValue,
+			Required:  false,
+		},
+	}
+}
+
 func KafkaConfig(opts EventBrokerOptions) events.KafkaConfig {
 	return events.KafkaConfig{
 		Brokers:              opts.BrokerURLs,
diff --git a/dev/docker-compose-sdp-anchor.yml b/dev/docker-compose-sdp-anchor.yml
index ec49e7946..f854571ea 100644
--- a/dev/docker-compose-sdp-anchor.yml
+++ b/dev/docker-compose-sdp-anchor.yml
@@ -34,6 +34,8 @@ services:
       SMS_SENDER_TYPE: DRY_RUN
       NETWORK_PASSPHRASE: "Test SDF Network ; September 2015"
       SEP10_SIGNING_PUBLIC_KEY: ${SEP10_SIGNING_PUBLIC_KEY}
+      SEP45_CONTRACT_ID: ${SEP45_CONTRACT_ID}
+      RPC_URL: "https://soroban-testnet.stellar.org"
       ANCHOR_PLATFORM_BASE_SEP_URL: http://localhost:8080
       ANCHOR_PLATFORM_BASE_PLATFORM_URL: http://anchor-platform:8085
       DISTRIBUTION_PUBLIC_KEY: ${DISTRIBUTION_PUBLIC_KEY}
@@ -108,7 +110,7 @@ services:
 
   anchor-platform:
     container_name: anchor-platform
-    image: stellar/anchor-platform:3.1.0
+    image: stellar/anchor-platform:3.2.0-beta.1
     depends_on:
       - db
     command: --sep-server --platform-server --platform linux/amd64
@@ -137,6 +139,10 @@ services:
       SEP10_WEB_AUTH_DOMAIN: "localhost:8080"
       # SEP10_CLIENT_ATTRIBUTION_REQUIRED: true  # RECOMMENDED
       # SEP10_CLIENT_ATTRIBUTION_ALLOW_LIST: "demo-wallet-server.stellar.org,https://example.com"  # RECOMMENDED
+      JAVA_TOOL_OPTIONS: "${SEP45_CONTRACT_ID:+-Dsep45.enabled=true}" #  Enable/disable SEP-45 based on SEP45_CONTRACT_ID
+      SEP45_WEB_AUTH_DOMAIN: ${SEP45_CONTRACT_ID:+localhost:8080}
+      SEP45_WEB_AUTH_CONTRACT_ID: ${SEP45_CONTRACT_ID}
+      SEP45_HOME_DOMAINS: ${SEP45_CONTRACT_ID:+localhost:8000, *.stellar.local:8000} # Comma separated list of home domains
       SEP24_ENABLED: "true"
       SEP24_INTERACTIVE_URL_BASE_URL: http://localhost:8000/wallet-registration/start
       SEP24_INTERACTIVE_URL_JWT_EXPIRATION: 1800  # 1800 seconds is 30 minutes
@@ -192,6 +198,7 @@ services:
       SECRET_PLATFORM_API_AUTH_SECRET: mySdpToAnchorPlatformSecret
       SECRET_SEP10_JWT_SECRET: jwt_secret_cQBnfcDSfvrphw8JBMcg1KfHRvvk3425
       SECRET_SEP10_SIGNING_SEED: ${SEP10_SIGNING_PRIVATE_KEY}
+      SECRET_SEP45_JWT_SECRET: jwt_secret_cQBnfcDSfvrphw8JBMcg1KfHRvvk3425
       SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET: jwt_secret_ducrCcqnKmIqG6mYG48Hqlf9TWb7CJh4
       SECRET_SEP24_MORE_INFO_URL_JWT_SECRET: jwt_secret_IyLxs6PW41Znaysr8ft8Wx326QJepCrQ
 
diff --git a/internal/integrationtests/docker/docker-compose-e2e-tests.yml b/internal/integrationtests/docker/docker-compose-e2e-tests.yml
index 4e154ecd4..237f0f9d0 100644
--- a/internal/integrationtests/docker/docker-compose-e2e-tests.yml
+++ b/internal/integrationtests/docker/docker-compose-e2e-tests.yml
@@ -33,7 +33,9 @@ services:
       SMS_SENDER_TYPE: DRY_RUN
       NETWORK_PASSPHRASE: ${NETWORK_PASSPHRASE:-Test SDF Network ; September 2015}
       HORIZON_URL: ${HORIZON_URL:-https://horizon-testnet.stellar.org}
+      RPC_URL: ${RPC_URL:-https://soroban-testnet.stellar.org}
       SEP10_SIGNING_PUBLIC_KEY: ${SEP10_SIGNING_PUBLIC_KEY}
+      SEP45_CONTRACT_ID: ${SEP45_CONTRACT_ID}
       ANCHOR_PLATFORM_BASE_SEP_URL: http://anchor-platform:8080
       ANCHOR_PLATFORM_BASE_PLATFORM_URL: http://anchor-platform:8085
       DISTRIBUTION_PUBLIC_KEY: ${DISTRIBUTION_PUBLIC_KEY}
@@ -166,7 +168,7 @@ services:
 
   anchor-platform:
     container_name: e2e-anchor-platform
-    image: stellar/anchor-platform:3.1.0
+    image: stellar/anchor-platform:3.2.0-beta.1
     command: --sep-server --platform-server --platform linux/amd64
     ports:
       - "8080:8080" # sep-server
@@ -194,6 +196,10 @@ services:
       SEP10_WEB_AUTH_DOMAIN: "localhost:8080"
       # SEP10_CLIENT_ATTRIBUTION_REQUIRED: true  # RECOMMENDED
       # SEP10_CLIENT_ATTRIBUTION_ALLOW_LIST: "demo-wallet-server.stellar.org,https://example.com"  # RECOMMENDED
+      JAVA_TOOL_OPTIONS: "${SEP45_CONTRACT_ID:+-Dsep45.enabled=true}" # Enable/disable SEP-45 based on SEP45_CONTRACT_ID
+      SEP45_WEB_AUTH_DOMAIN: ${SEP45_CONTRACT_ID:+localhost:8080}
+      SEP45_WEB_AUTH_CONTRACT_ID: ${SEP45_CONTRACT_ID}
+      SEP45_HOME_DOMAINS: ${SEP45_CONTRACT_ID:+localhost:8000, *.stellar.local:8000} # Comma separated list of home domains
       SEP24_ENABLED: "true"
       SEP24_INTERACTIVE_URL_BASE_URL: http://sdp-api:8000/wallet-registration/start
       SEP24_INTERACTIVE_URL_JWT_EXPIRATION: 1800  # 1800 seconds is 30 minutes
@@ -201,6 +207,7 @@ services:
       STELLAR_NETWORK_NETWORK: ${NETWORK_NETWORK:-TESTNET}
       STELLAR_NETWORK_NETWORK_PASSPHRASE: ${NETWORK_PASSPHRASE:-Test SDF Network ; September 2015}
       STELLAR_NETWORK_HORIZON_URL: ${HORIZON_URL:-https://horizon-testnet.stellar.org}
+      STELLAR_NETWORK_RPC_URL: ${RPC_URL:-https://soroban-testnet.stellar.org}
       SEP1_ENABLED: "true"
       SEP1_TOML_TYPE: url
       SEP1_TOML_VALUE: http://sdp-api:8000/.well-known/stellar.toml
@@ -251,6 +258,7 @@ services:
       SECRET_PLATFORM_API_AUTH_SECRET: mySdpToAnchorPlatformSecret
       SECRET_SEP10_JWT_SECRET: jwt_secret_cQBnfcDSfvrphw8JBMcg1KfHRvvk3425
       SECRET_SEP10_SIGNING_SEED: ${SEP10_SIGNING_PRIVATE_KEY}
+      SECRET_SEP45_JWT_SECRET: jwt_secret_cQBnfcDSfvrphw8JBMcg1KfHRvvk3425
       SECRET_SEP24_INTERACTIVE_URL_JWT_SECRET: jwt_secret_ducrCcqnKmIqG6mYG48Hqlf9TWb7CJh4
       SECRET_SEP24_MORE_INFO_URL_JWT_SECRET: jwt_secret_IyLxs6PW41Znaysr8ft8Wx326QJepCrQ
 
diff --git a/internal/serve/httphandler/stellar_toml_handler.go b/internal/serve/httphandler/stellar_toml_handler.go
index d74892ba9..6ae2f37b4 100644
--- a/internal/serve/httphandler/stellar_toml_handler.go
+++ b/internal/serve/httphandler/stellar_toml_handler.go
@@ -23,6 +23,7 @@ type StellarTomlHandler struct {
 	NetworkPassphrase           string
 	Models                      *data.Models
 	Sep10SigningPublicKey       string
+	Sep45ContractId             string
 	InstanceName                string
 }
 
@@ -48,16 +49,27 @@ func (s *StellarTomlHandler) buildGeneralInformation(ctx context.Context, req *h
 	}
 
 	webAuthEndpoint := s.AnchorPlatformBaseSepURL + "/auth"
+	webAuthForContractsEndpoint := s.AnchorPlatformBaseSepURL + "/sep45/auth"
 	transferServerSep0024 := s.AnchorPlatformBaseSepURL + "/sep24"
 
-	return fmt.Sprintf(`
-		ACCOUNTS=%s
-		SIGNING_KEY=%q
-		NETWORK_PASSPHRASE=%q
-		HORIZON_URL=%q
-		WEB_AUTH_ENDPOINT=%q
-		TRANSFER_SERVER_SEP0024=%q
-	`, accounts, s.Sep10SigningPublicKey, s.NetworkPassphrase, s.horizonURL(), webAuthEndpoint, transferServerSep0024)
+	var builder strings.Builder
+	builder.WriteString(fmt.Sprintf(`
+	ACCOUNTS=%s
+	SIGNING_KEY=%q
+	NETWORK_PASSPHRASE=%q
+	HORIZON_URL=%q
+	WEB_AUTH_ENDPOINT=%q
+	TRANSFER_SERVER_SEP0024=%q
+`, accounts, s.Sep10SigningPublicKey, s.NetworkPassphrase, s.horizonURL(), webAuthEndpoint, transferServerSep0024))
+
+	if s.Sep45ContractId != "" {
+		builder.WriteString(fmt.Sprintf(`
+		WEB_AUTH_CONTRACT_ID=%q
+		WEB_AUTH_FOR_CONTRACTS_ENDPOINT=%q
+		`, s.Sep45ContractId, webAuthForContractsEndpoint))
+	}
+
+	return builder.String()
 }
 
 func (s *StellarTomlHandler) buildOrganizationDocumentation(instanceName string) string {
diff --git a/internal/serve/httphandler/stellar_toml_handler_test.go b/internal/serve/httphandler/stellar_toml_handler_test.go
index 968756746..98b18c259 100644
--- a/internal/serve/httphandler/stellar_toml_handler_test.go
+++ b/internal/serve/httphandler/stellar_toml_handler_test.go
@@ -97,6 +97,50 @@ func Test_StellarTomlHandler_buildGeneralInformation(t *testing.T) {
 				`TRANSFER_SERVER_SEP0024="https://anchor-platform-domain/sep24"`,
 			},
 		},
+		{
+			name:              "pubnet with SEP-45 enabled (with tenant in context)",
+			isTenantInContext: true,
+			s: StellarTomlHandler{
+				// DistributionAccountResolver: <---- this is being injected in the test below
+				NetworkPassphrase:        network.PublicNetworkPassphrase,
+				Sep10SigningPublicKey:    "GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S",
+				Sep45ContractId:          "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX",
+				AnchorPlatformBaseSepURL: "https://anchor-platform-domain",
+			},
+			wantLines: []string{
+				fmt.Sprintf(`ACCOUNTS=[%q, "GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"]`, tenantDistAccPublicKey),
+				`SIGNING_KEY="GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"`,
+				fmt.Sprintf("NETWORK_PASSPHRASE=%q", network.PublicNetworkPassphrase),
+				fmt.Sprintf("HORIZON_URL=%q", horizonPubnetURL),
+				`WEB_AUTH_ENDPOINT="https://anchor-platform-domain/auth"`,
+				`TRANSFER_SERVER_SEP0024="https://anchor-platform-domain/sep24"`,
+				"",
+				`WEB_AUTH_CONTRACT_ID="CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX"`,
+				`WEB_AUTH_FOR_CONTRACTS_ENDPOINT="https://anchor-platform-domain/sep45/auth"`,
+			},
+		},
+		{
+			name:              "pubnet with SEP-45 enabled (without tenant in context)",
+			isTenantInContext: false,
+			s: StellarTomlHandler{
+				// DistributionAccountResolver: <---- this is being injected in the test below
+				NetworkPassphrase:        network.PublicNetworkPassphrase,
+				Sep10SigningPublicKey:    "GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S",
+				Sep45ContractId:          "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX",
+				AnchorPlatformBaseSepURL: "https://anchor-platform-domain",
+			},
+			wantLines: []string{
+				`ACCOUNTS=["GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"]`,
+				`SIGNING_KEY="GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"`,
+				fmt.Sprintf("NETWORK_PASSPHRASE=%q", network.PublicNetworkPassphrase),
+				fmt.Sprintf("HORIZON_URL=%q", horizonPubnetURL),
+				`WEB_AUTH_ENDPOINT="https://anchor-platform-domain/auth"`,
+				`TRANSFER_SERVER_SEP0024="https://anchor-platform-domain/sep24"`,
+				"",
+				`WEB_AUTH_CONTRACT_ID="CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX"`,
+				`WEB_AUTH_FOR_CONTRACTS_ENDPOINT="https://anchor-platform-domain/sep45/auth"`,
+			},
+		},
 		{
 			name:              "testnet (without tenant in context)",
 			isTenantInContext: false,
@@ -133,6 +177,50 @@ func Test_StellarTomlHandler_buildGeneralInformation(t *testing.T) {
 				`TRANSFER_SERVER_SEP0024="https://anchor-platform-domain/sep24"`,
 			},
 		},
+		{
+			name:              "testnet with SEP-45 enabled (with tenant in context)",
+			isTenantInContext: true,
+			s: StellarTomlHandler{
+				// DistributionAccountResolver: <---- this is being injected in the test below
+				NetworkPassphrase:        network.TestNetworkPassphrase,
+				Sep10SigningPublicKey:    "GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S",
+				Sep45ContractId:          "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX",
+				AnchorPlatformBaseSepURL: "https://anchor-platform-domain",
+			},
+			wantLines: []string{
+				fmt.Sprintf(`ACCOUNTS=[%q, "GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"]`, tenantDistAccPublicKey),
+				`SIGNING_KEY="GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"`,
+				fmt.Sprintf("NETWORK_PASSPHRASE=%q", network.TestNetworkPassphrase),
+				fmt.Sprintf("HORIZON_URL=%q", horizonTestnetURL),
+				`WEB_AUTH_ENDPOINT="https://anchor-platform-domain/auth"`,
+				`TRANSFER_SERVER_SEP0024="https://anchor-platform-domain/sep24"`,
+				"",
+				`WEB_AUTH_CONTRACT_ID="CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX"`,
+				`WEB_AUTH_FOR_CONTRACTS_ENDPOINT="https://anchor-platform-domain/sep45/auth"`,
+			},
+		},
+		{
+			name:              "testnet with SEP-45 enabled (without tenant in context)",
+			isTenantInContext: false,
+			s: StellarTomlHandler{
+				// DistributionAccountResolver: <---- this is being injected in the test below
+				NetworkPassphrase:        network.TestNetworkPassphrase,
+				Sep10SigningPublicKey:    "GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S",
+				Sep45ContractId:          "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX",
+				AnchorPlatformBaseSepURL: "https://anchor-platform-domain",
+			},
+			wantLines: []string{
+				`ACCOUNTS=["GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"]`,
+				`SIGNING_KEY="GAX46JJZ3NPUM2EUBTTGFM6ITDF7IGAFNBSVWDONPYZJREHFPP2I5U7S"`,
+				fmt.Sprintf("NETWORK_PASSPHRASE=%q", network.TestNetworkPassphrase),
+				fmt.Sprintf("HORIZON_URL=%q", horizonTestnetURL),
+				`WEB_AUTH_ENDPOINT="https://anchor-platform-domain/auth"`,
+				`TRANSFER_SERVER_SEP0024="https://anchor-platform-domain/sep24"`,
+				"",
+				`WEB_AUTH_CONTRACT_ID="CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX"`,
+				`WEB_AUTH_FOR_CONTRACTS_ENDPOINT="https://anchor-platform-domain/sep45/auth"`,
+			},
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/internal/serve/serve.go b/internal/serve/serve.go
index 053911c7e..fbbcdec25 100644
--- a/internal/serve/serve.go
+++ b/internal/serve/serve.go
@@ -31,6 +31,7 @@ import (
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/sep24frontend"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/validators"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/services"
+	"github.com/stellar/stellar-disbursement-platform-backend/internal/stellar"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
 	"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/pkg/auth"
@@ -74,6 +75,8 @@ type ServeOptions struct {
 	SubmitterEngine                 engine.SubmitterEngine
 	Sep10SigningPublicKey           string
 	Sep10SigningPrivateKey          string
+	Sep45ContractId                 string
+	RpcConfig                       stellar.RpcOptions
 	AnchorPlatformBaseSepURL        string
 	AnchorPlatformBasePlatformURL   string
 	AnchorPlatformOutgoingJWTSecret string
@@ -160,11 +163,37 @@ func (opts *ServeOptions) ValidateSecurity() error {
 	return nil
 }
 
+// ValidateRpc validates the RPC options.
+func (opts *ServeOptions) ValidateRpc() error {
+	if opts.RpcConfig.RpcURL == "" && (opts.RpcConfig.RpcRequestHeaderKey != "" || opts.RpcConfig.RpcRequestHeaderValue != "") {
+		return fmt.Errorf("RPC URL must be set when RPC request header key or value is set")
+	}
+
+	if opts.RpcConfig.RpcRequestHeaderKey != "" && opts.RpcConfig.RpcRequestHeaderValue == "" {
+		return fmt.Errorf("RPC request header value must be set when RPC request header key is set")
+	}
+
+	if opts.RpcConfig.RpcRequestHeaderKey == "" && opts.RpcConfig.RpcRequestHeaderValue != "" {
+		return fmt.Errorf("RPC request header key must be set when RPC request header value is set")
+	}
+
+	// Feature specific validation
+	if opts.Sep45ContractId != "" && opts.RpcConfig.RpcURL == "" {
+		return fmt.Errorf("RPC URL must be set when SEP-45 contract ID is set")
+	}
+
+	return nil
+}
+
 func Serve(opts ServeOptions, httpServer HTTPServerInterface) error {
 	if err := opts.ValidateSecurity(); err != nil {
 		return fmt.Errorf("validating security options: %w", err)
 	}
 
+	if err := opts.ValidateRpc(); err != nil {
+		return fmt.Errorf("validating RPC options: %w", err)
+	}
+
 	if err := opts.SetupDependencies(); err != nil {
 		return fmt.Errorf("starting dependencies: %w", err)
 	}
@@ -484,6 +513,7 @@ func handleHTTP(o ServeOptions) *chi.Mux {
 			NetworkPassphrase:           o.NetworkPassphrase,
 			Models:                      o.Models,
 			Sep10SigningPublicKey:       o.Sep10SigningPublicKey,
+			Sep45ContractId:             o.Sep45ContractId,
 			InstanceName:                o.InstanceName,
 		}.ServeHTTP)
 
diff --git a/internal/serve/serve_test.go b/internal/serve/serve_test.go
index 978d6d1d6..b15b94073 100644
--- a/internal/serve/serve_test.go
+++ b/internal/serve/serve_test.go
@@ -30,6 +30,7 @@ import (
 	monitorMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor/mocks"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/middleware"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/publicfiles"
+	"github.com/stellar/stellar-disbursement-platform-backend/internal/stellar"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine"
 	preconditionsMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/preconditions/mocks"
 	"github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/signing"
@@ -173,6 +174,166 @@ func Test_ServeOptions_ValidateSecurity(t *testing.T) {
 	})
 }
 
+func Test_Serve_callsValidateRpc(t *testing.T) {
+	t.Run("RPC Header Key + Value without URL: should return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{
+			RpcRequestHeaderKey:   "Authorization: Bearer",
+			RpcRequestHeaderValue: "test",
+		}
+		serveOptions := ServeOptions{
+			RpcConfig: rpcOptions,
+		}
+
+		err := serveOptions.ValidateRpc()
+		require.EqualError(t, err, "RPC URL must be set when RPC request header key or value is set")
+	})
+
+	t.Run("RPC Header Key without URL: should return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{
+			RpcRequestHeaderKey: "Authorization: Bearer",
+		}
+
+		serveOptions := ServeOptions{
+			RpcConfig: rpcOptions,
+		}
+		err := serveOptions.ValidateRpc()
+		require.EqualError(t, err, "RPC URL must be set when RPC request header key or value is set")
+	})
+
+	t.Run("RPC Header Value without URL: should return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{
+			RpcRequestHeaderValue: "test",
+		}
+
+		serveOptions := ServeOptions{
+			RpcConfig: rpcOptions,
+		}
+		err := serveOptions.ValidateRpc()
+		require.EqualError(t, err, "RPC URL must be set when RPC request header key or value is set")
+	})
+
+	t.Run("RPC URL + Header Key without Value: should return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{
+			RpcURL:              "http://localhost:8000",
+			RpcRequestHeaderKey: "Authorization: Bearer",
+		}
+
+		serveOptions := ServeOptions{
+			RpcConfig: rpcOptions,
+		}
+		err := serveOptions.ValidateRpc()
+		require.EqualError(t, err, "RPC request header value must be set when RPC request header key is set")
+	})
+
+	t.Run("RPC URL + Header Value without Value: should return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{
+			RpcURL:                "http://localhost:8000",
+			RpcRequestHeaderValue: "test",
+		}
+
+		serveOptions := ServeOptions{
+			RpcConfig: rpcOptions,
+		}
+		err := serveOptions.ValidateRpc()
+		require.EqualError(t, err, "RPC request header key must be set when RPC request header value is set")
+	})
+
+	t.Run("RPC URL without Headers: should not return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{
+			RpcURL: "http://localhost:8000",
+		}
+
+		serveOptions := ServeOptions{
+			RpcConfig: rpcOptions,
+		}
+		err := serveOptions.ValidateRpc()
+		require.NoError(t, err)
+	})
+
+	t.Run("RPC URL + Headers: should not return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{
+			RpcURL:                "http://localhost:8000",
+			RpcRequestHeaderKey:   "Authorization: Bearer",
+			RpcRequestHeaderValue: "test",
+		}
+
+		serveOptions := ServeOptions{
+			RpcConfig: rpcOptions,
+		}
+		err := serveOptions.ValidateRpc()
+		require.NoError(t, err)
+	})
+
+	t.Run("SEP-45 contract ID without RPC URL: should return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{}
+
+		serveOptions := ServeOptions{
+			RpcConfig:       rpcOptions,
+			Sep45ContractId: "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX",
+		}
+		err := serveOptions.ValidateRpc()
+		require.EqualError(t, err, "RPC URL must be set when SEP-45 contract ID is set")
+	})
+
+	t.Run("SEP-45 contract ID with RPC URL: should not return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{
+			RpcURL: "http://localhost:8000",
+		}
+
+		serveOptions := ServeOptions{
+			RpcConfig:       rpcOptions,
+			Sep45ContractId: "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX",
+		}
+		err := serveOptions.ValidateRpc()
+		require.NoError(t, err)
+	})
+
+	t.Run("SEP-45 contract ID with RPC URL and Headers: should not return error", func(t *testing.T) {
+		buf := new(strings.Builder)
+		log.DefaultLogger.SetOutput(buf)
+
+		rpcOptions := stellar.RpcOptions{
+			RpcURL:                "http://localhost:8000",
+			RpcRequestHeaderKey:   "Authorization: Bearer",
+			RpcRequestHeaderValue: "test",
+		}
+
+		serveOptions := ServeOptions{
+			RpcConfig:       rpcOptions,
+			Sep45ContractId: "CD3LA6RKF5D2FN2R2L57MWXLBRSEWWENE74YBEFZSSGNJRJGICFGQXMX",
+		}
+		err := serveOptions.ValidateRpc()
+		require.NoError(t, err)
+	})
+}
+
 func Test_handleHTTP_Health(t *testing.T) {
 	dbt := dbtest.Open(t)
 	defer dbt.Close()
diff --git a/internal/stellar/rpc.go b/internal/stellar/rpc.go
new file mode 100644
index 000000000..dd3e3d4c4
--- /dev/null
+++ b/internal/stellar/rpc.go
@@ -0,0 +1,11 @@
+package stellar
+
+// RpcOptions contains the configuration options for the Stellar RPC server.
+type RpcOptions struct {
+	// URL of the Stellar RPC server where this application will communicate with.
+	RpcURL string
+	// The key of the request header to be used for authentication with the RPC server.
+	RpcRequestHeaderKey string
+	// The value of the request header to be used for authentication with the RPC server.
+	RpcRequestHeaderValue string
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR upgrades the Anchor Platform to 3.2.0-beta.1 and introduces new configuration options for RPC and SEP‑45 support. Key changes include:  
- Adding a new RpcOptions type and corresponding validation logic.  
- Integrating SEP‑45 contract ID handling in both server and TOML generation logic.  
- Updating tests, docker-compose files, and CLI configuration to accommodate these new options.

### Reviewed Changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                                        | Description                                                     |
| ----------------------------------------------------------- | --------------------------------------------------------------- |
| internal/stellar/rpc.go                                     | Added RpcOptions structure to encapsulate new RPC parameters.   |
| internal/serve/serve.go                                     | Extended ServeOptions with RpcConfig and SEP45ContractId, and added ValidateRpc(). |
| internal/serve/serve_test.go                                | Added tests to verify RPC configuration validation errors.       |
| internal/serve/httphandler/stellar_toml_handler.go          | Modified TOML handler to include SEP‑45 fields when set.           |
| internal/serve/httphandler/stellar_toml_handler_test.go     | Updated tests to check new SEP‑45 outputs in TOML.                  |
| internal/integrationtests/docker/docker-compose-e2e-tests.yml| Updated environment variables and image version for AP upgrade.    |
| internal/dependencyinjection/tx_submitter_engine.go         | Extended engine options to include RPC settings.                  |
| dev/docker-compose-tss.yml & dev/docker-compose-sdp-anchor.yml| Added RPC_URL and SEP45_CONTRACT_ID configuration.                  |
| cmd/utils/shared_config_options.go, cmd/utils/custom_set_value*.go, cmd/serve.go | Added CLI configuration options and tests for the new RPC and SEP‑45 settings. |
</details>


<details>
<summary>Files not reviewed (1)</summary>

* **.env.example**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**internal/serve/serve.go:79**
* [nitpick] Consider renaming 'RpcConfig' to 'RPCConfig' to follow the conventional uppercase acronym style for 'RPC', for consistency across the codebase.
```
RpcConfig                       stellar.RpcOptions
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T21:15:45Z
user: login: philipliu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T14:45:28Z
user: login: marwen-abid
body: LGTM! 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T16:30:33Z
user: login: philipliu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T16:39:16Z
user: login: philipliu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T20:48:22Z

Review comments:
diff_hunk: @@ -48,16 +49,26 @@ func (s *StellarTomlHandler) buildGeneralInformation(ctx context.Context, req *h
 	}
 
 	webAuthEndpoint := s.AnchorPlatformBaseSepURL + "/auth"
+	webAuthForContractsEndpoint := s.AnchorPlatformBaseSepURL + "/sep45/auth"
 	transferServerSep0024 := s.AnchorPlatformBaseSepURL + "/sep24"
 
-	return fmt.Sprintf(`
-		ACCOUNTS=%s
-		SIGNING_KEY=%q
-		NETWORK_PASSPHRASE=%q
-		HORIZON_URL=%q
-		WEB_AUTH_ENDPOINT=%q
-		TRANSFER_SERVER_SEP0024=%q
-	`, accounts, s.Sep10SigningPublicKey, s.NetworkPassphrase, s.horizonURL(), webAuthEndpoint, transferServerSep0024)
+	result := fmt.Sprintf(`
+	ACCOUNTS=%s
+	SIGNING_KEY=%q
+	NETWORK_PASSPHRASE=%q
+	HORIZON_URL=%q
+	WEB_AUTH_ENDPOINT=%q
+	TRANSFER_SERVER_SEP0024=%q
+`, accounts, s.Sep10SigningPublicKey, s.NetworkPassphrase, s.horizonURL(), webAuthEndpoint, transferServerSep0024)
+
+	if s.Sep45ContractId != "" {
+		result += fmt.Sprintf(`
+		WEB_AUTH_CONTRACT_ID=%q
+		WEB_AUTH_FOR_CONTRACTS_ENDPOINT=%q
+		`, s.Sep45ContractId, webAuthForContractsEndpoint)
+	}
+
+	return result
path: internal/serve/httphandler/stellar_toml_handler.go
user: login: Copilot
body: [nitpick] Consider refactoring the multiline string concatenation for SEP‑45 fields to ensure consistent formatting and avoid unintended whitespace in the generated TOML.
```suggestion
	var builder strings.Builder
	builder.WriteString(fmt.Sprintf(`
	ACCOUNTS=%s
	SIGNING_KEY=%q
	NETWORK_PASSPHRASE=%q
	HORIZON_URL=%q
	WEB_AUTH_ENDPOINT=%q
	TRANSFER_SERVER_SEP0024=%q
`, accounts, s.Sep10SigningPublicKey, s.NetworkPassphrase, s.horizonURL(), webAuthEndpoint, transferServerSep0024))

	if s.Sep45ContractId != "" {
		builder.WriteString(fmt.Sprintf(`
		WEB_AUTH_CONTRACT_ID=%q
		WEB_AUTH_FOR_CONTRACTS_ENDPOINT=%q
		`, s.Sep45ContractId, webAuthForContractsEndpoint))
	}

	return builder.String()
```
created_at: 2025-05-02T21:15:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,6 +140,10 @@ services:
       SEP10_WEB_AUTH_DOMAIN: "localhost:8080"
       # SEP10_CLIENT_ATTRIBUTION_REQUIRED: true  # RECOMMENDED
       # SEP10_CLIENT_ATTRIBUTION_ALLOW_LIST: "demo-wallet-server.stellar.org,https://example.com"  # RECOMMENDED
+      JAVA_TOOL_OPTIONS: "${SEP45_CONTRACT_ID:+-Dsep45.enabled=true} ${SEP45_CONTRACT_ID:- -Dsep45.enabled=false}" #  Enable/disable SEP-45 based on SEP45_CONTRACT_ID
path: dev/docker-compose-sdp-anchor.yml
user: login: philipliu
body: `SEP45_ENABLED: ${SEP45_CONTRACT_ID:+true}` does not work because when it is not set, `SEP45_ENABLED` will be set to an empty string and is not parseable by the Anchor Platform's config manager.
created_at: 2025-05-03T14:45:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,14 +181,45 @@ func SetConfigOptionStellarPrivateKey(co *config.ConfigOption) error {
 	return nil
 }
 
+func SetConfigOptionStellarContractId(co *config.ConfigOption) error {
+	contractId := viper.GetString(co.Name)
+	contractId = strings.TrimSpace(contractId)
+
+	if !co.Required && contractId == "" {
path: cmd/utils/custom_set_value.go
user: login: marwen-abid
body: ❓I'm not sure about the answer here. But I believe that this check is redundant because running `configOpts.Require()` does this for all configurations so we don't need to check manually. 
created_at: 2025-05-05T15:41:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,14 +181,45 @@ func SetConfigOptionStellarPrivateKey(co *config.ConfigOption) error {
 	return nil
 }
 
+func SetConfigOptionStellarContractId(co *config.ConfigOption) error {
+	contractId := viper.GetString(co.Name)
+	contractId = strings.TrimSpace(contractId)
+
+	if !co.Required && contractId == "" {
+		return nil
+	}
+
+	if contractId == "" {
+		return fmt.Errorf("contract id cannot be empty in %s", co.Name)
+	}
+
+	isValid := strkey.IsValidContractAddress(contractId)
+	if !isValid {
+		return fmt.Errorf("error validating contract id in %s: %q", co.Name, contractId)
path: cmd/utils/custom_set_value.go
user: login: marwen-abid
body: ```suggestion
		return fmt.Errorf("validating contract id in %s: %q", co.Name, contractId)
```
nit: As a general guideline we avoid adding `error` at the beginning of a returned error because it will be redundant once wrapped a few times. 
`.e.g `error validationg...  :error creating: error fetching..` 
created_at: 2025-05-05T15:43:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,6 +140,10 @@ services:
       SEP10_WEB_AUTH_DOMAIN: "localhost:8080"
       # SEP10_CLIENT_ATTRIBUTION_REQUIRED: true  # RECOMMENDED
       # SEP10_CLIENT_ATTRIBUTION_ALLOW_LIST: "demo-wallet-server.stellar.org,https://example.com"  # RECOMMENDED
+      JAVA_TOOL_OPTIONS: "${SEP45_CONTRACT_ID:+-Dsep45.enabled=true} ${SEP45_CONTRACT_ID:- -Dsep45.enabled=false}" #  Enable/disable SEP-45 based on SEP45_CONTRACT_ID
+      SEP45_WEB_AUTH_DOMAIN: ${SEP45_CONTRACT_ID:+localhost:8080}
+      SEP45_WEB_AUTH_CONTRACT_ID: ${SEP45_CONTRACT_ID}
+      SEP45_HOME_DOMAINS: ${SEP45_CONTRACT_ID:+localhost:8000, *.stellar.local:8000} # Comma separated list of home domains
       SEP24_ENABLED: "true"
path: dev/docker-compose-sdp-anchor.yml
user: login: marwen-abid
body: ❓Does anchor not need `STELLAR_NETWORK_RPC_URL`, I saw that passed to e2e tests ? 
created_at: 2025-05-05T15:58:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -138,6 +140,10 @@ services:
       SEP10_WEB_AUTH_DOMAIN: "localhost:8080"
       # SEP10_CLIENT_ATTRIBUTION_REQUIRED: true  # RECOMMENDED
       # SEP10_CLIENT_ATTRIBUTION_ALLOW_LIST: "demo-wallet-server.stellar.org,https://example.com"  # RECOMMENDED
+      JAVA_TOOL_OPTIONS: "${SEP45_CONTRACT_ID:+-Dsep45.enabled=true} ${SEP45_CONTRACT_ID:- -Dsep45.enabled=false}" #  Enable/disable SEP-45 based on SEP45_CONTRACT_ID
+      SEP45_WEB_AUTH_DOMAIN: ${SEP45_CONTRACT_ID:+localhost:8080}
+      SEP45_WEB_AUTH_CONTRACT_ID: ${SEP45_CONTRACT_ID}
+      SEP45_HOME_DOMAINS: ${SEP45_CONTRACT_ID:+localhost:8000, *.stellar.local:8000} # Comma separated list of home domains
       SEP24_ENABLED: "true"
path: dev/docker-compose-sdp-anchor.yml
user: login: philipliu
body: Like the Horizon URL, it defaults to the testnet instance if it's not specified. I passed it into the e2e tests for consistency because `STELLAR_NETWORK.HORIZON_URL` was specified, but it's not required.
created_at: 2025-05-05T16:39:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,14 +181,45 @@ func SetConfigOptionStellarPrivateKey(co *config.ConfigOption) error {
 	return nil
 }
 
+func SetConfigOptionStellarContractId(co *config.ConfigOption) error {
+	contractId := viper.GetString(co.Name)
+	contractId = strings.TrimSpace(contractId)
+
+	if !co.Required && contractId == "" {
path: cmd/utils/custom_set_value.go
user: login: philipliu
body: You're right, it's normally redundant. The reason this check is required is that the new configs are conditionally set based on the presence of `SEP45_CONTRACT_ID` in Docker Compose. If it's not set, an empty string will be passed to these setters and an error.

I think it's safe to keep these in until I find a workaround, but in the worst case, it's just a redundant check.
created_at: 2025-05-05T20:48:22Z
author_association: CONTRIBUTOR

PR comments:
user: login: stellar-jenkins
created_at: 2025-05-02T21:18:57Z
body: **stellar-disbursement-platform-backend-preview is available here:**<br>SDP:      https://sdp-backend-pr663.previews.kube001.services.stellar-ops.com/health<br>AP:       https://sdp-ap-pr663.previews.kube001.services.stellar-ops.com/health<br>Frontend: https://sdp-backend-dashboard-pr663.previews.kube001.services.stellar-ops.com
author_association: NONE
user: login: stellar-jenkins
created_at: 2025-05-02T21:19:21Z
body: **Something went wrong with PR preview build please check**
author_association: NONE
user: login: stellar-jenkins
created_at: 2025-05-02T21:20:29Z
body: **Something went wrong with PR preview build please check**
author_association: NONE
user: login: stellar-jenkins
created_at: 2025-05-02T23:24:38Z
body: **stellar-disbursement-platform-backend-preview is available here:**<br>SDP:      https://sdp-backend-pr663.previews.kube001.services.stellar-ops.com/health<br>AP:       https://sdp-ap-pr663.previews.kube001.services.stellar-ops.com/health<br>Frontend: https://sdp-backend-dashboard-pr663.previews.kube001.services.stellar-ops.com
author_association: NONE
user: login: stellar-jenkins
created_at: 2025-05-02T23:24:55Z
body: **Something went wrong with PR preview build please check**
author_association: NONE
user: login: stellar-jenkins
created_at: 2025-05-02T23:40:23Z
body: **stellar-disbursement-platform-backend-preview is available here:**<br>SDP:      https://sdp-backend-pr663.previews.kube001.services.stellar-ops.com/health<br>AP:       https://sdp-ap-pr663.previews.kube001.services.stellar-ops.com/health<br>Frontend: https://sdp-backend-dashboard-pr663.previews.kube001.services.stellar-ops.com
author_association: NONE
user: login: stellar-jenkins
created_at: 2025-05-03T03:19:59Z
body: **stellar-disbursement-platform-backend-preview is available here:**<br>SDP:      https://sdp-backend-pr663.previews.kube001.services.stellar-ops.com/health<br>AP:       https://sdp-ap-pr663.previews.kube001.services.stellar-ops.com/health<br>Frontend: https://sdp-backend-dashboard-pr663.previews.kube001.services.stellar-ops.com
author_association: NONE
user: login: stellar-jenkins
created_at: 2025-05-03T14:45:45Z
body: **stellar-disbursement-platform-backend-preview is available here:**<br>SDP:      https://sdp-backend-pr663.previews.kube001.services.stellar-ops.com/health<br>AP:       https://sdp-ap-pr663.previews.kube001.services.stellar-ops.com/health<br>Frontend: https://sdp-backend-dashboard-pr663.previews.kube001.services.stellar-ops.com
author_association: NONE
user: login: stellar-jenkins
created_at: 2025-05-05T20:52:17Z
body: **stellar-disbursement-platform-backend-preview is available here:**<br>SDP:      https://sdp-backend-pr663.previews.kube001.services.stellar-ops.com/health<br>AP:       https://sdp-ap-pr663.previews.kube001.services.stellar-ops.com/health<br>Frontend: https://sdp-backend-dashboard-pr663.previews.kube001.services.stellar-ops.com
author_association: NONE