diff --git a/CHANGELOG.md b/CHANGELOG.md index 515fdb7bc..7cee42d85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - Default Max Open Connections per pool changed from 30 to 20 to prevent idle connection buildup in multi-tenant deployments [#932](https://github.com/stellar/stellar-disbursement-platform-backend/pull/932) - Make docker compose environment variables configurable via `.env` file and add documentation [#953](https://github.com/stellar/stellar-disbursement-platform-backend/pull/953) - Update Stellar Go SDK dependency from `github.com/stellar/go` to `github.com/stellar/go-stellar-sdk` [#956](https://github.com/stellar/stellar-disbursement-platform-backend/pull/956) +- Remove case insensitivity from asset code comparisons [#967](https://github.com/stellar/stellar-disbursement-platform-backend/pull/967) ### Fixed diff --git a/internal/data/assets.go b/internal/data/assets.go index 02467e5ff..6ca020705 100644 --- a/internal/data/assets.go +++ b/internal/data/assets.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "fmt" - "slices" "strings" "time" @@ -44,15 +43,15 @@ func AssetColumnNames(tableReference, resultAlias string, includeDates bool) str // IsNative returns true if the asset is the native asset (XLM). func (a Asset) IsNative() bool { return strings.TrimSpace(a.Issuer) == "" && - slices.Contains([]string{"XLM", "NATIVE"}, strings.ToUpper(a.Code)) + (a.Code == "XLM" || a.Code == "NATIVE") } -// Equals returns true if the asset is the same as the other asset. Case-insensitive. +// Equals returns true if the asset is the same as the other asset. func (a Asset) Equals(other Asset) bool { if a.IsNative() && other.IsNative() { return true } - return strings.EqualFold(a.Code, other.Code) && strings.EqualFold(a.Issuer, other.Issuer) + return a.Code == other.Code && strings.EqualFold(a.Issuer, other.Issuer) } func (a Asset) EqualsHorizonAsset(horizonAsset base.Asset) bool { @@ -60,7 +59,7 @@ func (a Asset) EqualsHorizonAsset(horizonAsset base.Asset) bool { return true } - return strings.EqualFold(a.Code, horizonAsset.Code) && strings.EqualFold(a.Issuer, horizonAsset.Issuer) + return a.Code == horizonAsset.Code && strings.EqualFold(a.Issuer, horizonAsset.Issuer) } func (a Asset) ToBasicAsset() txnbuild.Asset { diff --git a/internal/data/assets_test.go b/internal/data/assets_test.go index 9f2ea4ee7..45c5d7e7c 100644 --- a/internal/data/assets_test.go +++ b/internal/data/assets_test.go @@ -112,10 +112,11 @@ func Test_Asset_Equals(t *testing.T) { }{ {Asset{Code: "XLM"}, Asset{Code: "XLM"}, true}, {Asset{Code: "NATIVE"}, Asset{Code: "XLM"}, true}, - {Asset{Code: "XLM"}, Asset{Code: "xlm"}, true}, + {Asset{Code: "NATIVE"}, Asset{Code: "native"}, false}, + {Asset{Code: "XLM"}, Asset{Code: "xlm"}, false}, {Asset{Code: "XLM"}, Asset{Code: "ABC"}, false}, - {Asset{Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", Code: "USDC"}, Asset{Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", Code: "usdc"}, true}, - {Asset{Issuer: "gbbD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", Code: "USDC"}, Asset{Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", Code: "usdc"}, true}, + {Asset{Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", Code: "USDC"}, Asset{Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", Code: "usdc"}, false}, + {Asset{Issuer: "gbbD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", Code: "USDC"}, Asset{Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", Code: "USDC"}, true}, {Asset{Issuer: "Issuer1", Code: "ABC"}, Asset{Issuer: "Issuer2", Code: "ABC"}, false}, {Asset{Issuer: "Issuer1", Code: "ABC"}, Asset{Issuer: "Issuer1", Code: "XYZ"}, false}, } @@ -138,16 +139,16 @@ func Test_Asset_EqualsHorizonAsset(t *testing.T) { expectedResult bool }{ { - name: "🟢 native assets", + name: "🟢 XLM alias is equal to native type", localAsset: Asset{Code: "XLM"}, horizonAsset: base.Asset{Type: "native"}, expectedResult: true, }, { - name: "🟢 native asset 2", - localAsset: Asset{Code: "NATIVE"}, + name: "🔴 xlm alias is not equal to native type", + localAsset: Asset{Code: "xlm"}, horizonAsset: base.Asset{Type: "native"}, - expectedResult: true, + expectedResult: false, }, { name: "🟢 issued assets are equal", @@ -156,31 +157,43 @@ func Test_Asset_EqualsHorizonAsset(t *testing.T) { expectedResult: true, }, { - name: "🟢 issued assets are equal2", + name: "🟢 issued assets with different case in issuer are equal", + localAsset: Asset{Code: "USDC", Issuer: "gbbD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, + horizonAsset: base.Asset{Type: "credit_alphanum4", Code: "USDC", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, + expectedResult: true, + }, + { + name: "🔴 issued assets with different case in code are not equal", localAsset: Asset{Code: "usdc", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, horizonAsset: base.Asset{Type: "credit_alphanum4", Code: "USdc", Issuer: "gbbD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, + expectedResult: false, + }, + { + name: "🟢 NATIVE asset alias is equal to native type", + localAsset: Asset{Code: "NATIVE"}, + horizonAsset: base.Asset{Type: "native"}, expectedResult: true, }, { - name: "🔴 native asset != issued asset", - localAsset: Asset{Code: "XLM"}, - horizonAsset: base.Asset{Type: "credit_alphanum4", Code: "NATIVE", Issuer: "issuer"}, + name: "🔴 native asset alias is not equal to native type", + localAsset: Asset{Code: "native"}, + horizonAsset: base.Asset{Type: "native"}, expectedResult: false, }, { - name: "🔴 issued asset != native asset", + name: "🔴 issued asset is not equal to native asset", localAsset: Asset{Code: "USDC", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, horizonAsset: base.Asset{Type: "native"}, expectedResult: false, }, { - name: "🔴 issued asset != issued asset", + name: "🔴 issued asset is not equal to issued asset with different code", localAsset: Asset{Code: "USDC", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, horizonAsset: base.Asset{Type: "credit_alphanum4", Code: "EUROC", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, expectedResult: false, }, { - name: "🔴 issued asset != issued asset 2", + name: "🔴 issued asset is not equal to issued asset with different issuer", localAsset: Asset{Code: "USDC", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, horizonAsset: base.Asset{Type: "credit_alphanum4", Code: "USDC", Issuer: "another-issuer"}, expectedResult: false, diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go index 2eac927da..1b3d2fdb1 100644 --- a/internal/data/fixtures.go +++ b/internal/data/fixtures.go @@ -9,7 +9,6 @@ import ( "image" "image/color" "math/big" - "strings" "testing" "time" @@ -28,7 +27,7 @@ const ( func CreateAssetFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, code, issuer string) *Asset { issuerAddress := issuer - if issuerAddress == "" && strings.ToUpper(code) != "XLM" { + if issuerAddress == "" && code != "XLM" && code != "NATIVE" { issuer, err := utils.RandomString(56) require.NoError(t, err) issuerAddress = issuer diff --git a/internal/serve/httphandler/assets_handler.go b/internal/serve/httphandler/assets_handler.go index 02fb2f3a1..6d35e186f 100644 --- a/internal/serve/httphandler/assets_handler.go +++ b/internal/serve/httphandler/assets_handler.go @@ -23,13 +23,16 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httperror" "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/services/assets" "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine" tssUtils "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/utils" "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" "github.com/stellar/stellar-disbursement-platform-backend/pkg/schema" ) -const stellarNativeAssetCode = "XLM" +func isNativeAssetCode(code string) bool { + return code == assets.XLMAssetCode || code == assets.XLMAssetCodeAlias +} var errCouldNotRemoveTrustline = errors.New("could not remove trustline") @@ -144,12 +147,12 @@ func (c AssetsHandler) CreateAsset(w http.ResponseWriter, r *http.Request) { return } - assetCode := strings.TrimSpace(strings.ToUpper(assetRequest.Code)) + assetCode := strings.TrimSpace(assetRequest.Code) assetIssuer := strings.TrimSpace(assetRequest.Issuer) v := validators.NewValidator() v.Check(assetCode != "", "code", "code is required") - if assetCode != stellarNativeAssetCode { + if !isNativeAssetCode(assetCode) { v.Check(strkey.IsValidEd25519PublicKey(assetIssuer), "issuer", "issuer is invalid") } @@ -267,7 +270,7 @@ func (c AssetsHandler) handleUpdateAssetTrustlineForDistributionAccount( changeTrustOperations := make([]*txnbuild.ChangeTrust, 0) // remove asset - if assetToRemoveTrustline != nil && strings.ToUpper(assetToRemoveTrustline.Code) != stellarNativeAssetCode { + if assetToRemoveTrustline != nil && !isNativeAssetCode(assetToRemoveTrustline.Code) { for _, balance := range acc.Balances { if balance.Asset.Code == assetToRemoveTrustline.Code && balance.Asset.Issuer == assetToRemoveTrustline.Issuer { assetToRemoveTrustlineBalance, parseBalErr := amount.ParseInt64(balance.Balance) @@ -305,7 +308,7 @@ func (c AssetsHandler) handleUpdateAssetTrustlineForDistributionAccount( } // add asset - if assetToAddTrustline != nil && strings.ToUpper(assetToAddTrustline.Code) != stellarNativeAssetCode { + if assetToAddTrustline != nil && !isNativeAssetCode(assetToAddTrustline.Code) { var assetToAddTrustlineFound bool for _, balance := range acc.Balances { if balance.Asset.Code == assetToAddTrustline.Code && balance.Asset.Issuer == assetToAddTrustline.Issuer { diff --git a/internal/serve/validators/wallet_validator.go b/internal/serve/validators/wallet_validator.go index 2e50e123c..961476c8a 100644 --- a/internal/serve/validators/wallet_validator.go +++ b/internal/serve/validators/wallet_validator.go @@ -288,7 +288,7 @@ func (wv *WalletValidator) inferAssetType(asset AssetReference) AssetReference { // Inference logic for backward compatibility result := asset - if strings.ToUpper(asset.Code) == assets.XLMAssetCode && asset.Issuer == "" { + if (asset.Code == assets.XLMAssetCode || asset.Code == assets.XLMAssetCodeAlias) && asset.Issuer == "" { result.Type = string(AssetReferenceTypeNative) result.Code = "" return result diff --git a/internal/serve/validators/wallet_validator_test.go b/internal/serve/validators/wallet_validator_test.go index d42e62456..608bd9db8 100644 --- a/internal/serve/validators/wallet_validator_test.go +++ b/internal/serve/validators/wallet_validator_test.go @@ -635,8 +635,7 @@ func TestWalletValidator_inferAssetType(t *testing.T) { Issuer: "", }, expected: AssetReference{ - Type: "native", - Code: "", + Code: "xlm", Issuer: "", }, }, @@ -693,8 +692,7 @@ func TestWalletValidator_inferAssetType(t *testing.T) { Code: "XLm", }, expected: AssetReference{ - Type: "native", - Code: "", + Code: "XLm", }, }, } diff --git a/internal/services/assets/assets_pubnet.go b/internal/services/assets/assets_pubnet.go index 3854faede..a567a1416 100644 --- a/internal/services/assets/assets_pubnet.go +++ b/internal/services/assets/assets_pubnet.go @@ -34,6 +34,8 @@ var EURCAssetPubnet = data.Asset{ const XLMAssetCode = "XLM" +const XLMAssetCodeAlias = "NATIVE" + var XLMAsset = data.Asset{ Code: XLMAssetCode, Issuer: "", diff --git a/internal/services/payment_to_submitter_service.go b/internal/services/payment_to_submitter_service.go index f964e07fd..7cf017825 100644 --- a/internal/services/payment_to_submitter_service.go +++ b/internal/services/payment_to_submitter_service.go @@ -11,6 +11,7 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/internal/circle" "github.com/stellar/stellar-disbursement-platform-backend/internal/data" "github.com/stellar/stellar-disbursement-platform-backend/internal/sdpcontext" + "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" "github.com/stellar/stellar-disbursement-platform-backend/internal/services/paymentdispatchers" "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/signing" txSubStore "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/store" @@ -167,7 +168,10 @@ func validatePaymentReadyForSending(p *data.Payment) error { return fmt.Errorf("payment asset code is empty for payment %s", p.ID) } // 3. payment.asset.Issuer is used as transaction.AssetIssuer - if strings.TrimSpace(p.Asset.Issuer) == "" && strings.TrimSpace(strings.ToUpper(p.Asset.Code)) != "XLM" { + codeTrimmed := strings.TrimSpace(p.Asset.Code) + if strings.TrimSpace(p.Asset.Issuer) == "" && + codeTrimmed != assets.XLMAssetCode && + codeTrimmed != assets.XLMAssetCodeAlias { return fmt.Errorf("payment asset issuer is empty for payment %s", p.ID) } // 4. payment.Amount is used as transaction.Amount diff --git a/internal/services/send_receiver_wallets_invite_service.go b/internal/services/send_receiver_wallets_invite_service.go index 8c9d18274..72051e7d3 100644 --- a/internal/services/send_receiver_wallets_invite_service.go +++ b/internal/services/send_receiver_wallets_invite_service.go @@ -6,7 +6,6 @@ import ( "html/template" "net/url" "path" - "slices" "strings" "time" @@ -18,6 +17,7 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/internal/data" "github.com/stellar/stellar-disbursement-platform-backend/internal/message" "github.com/stellar/stellar-disbursement-platform-backend/internal/sdpcontext" + "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" ) @@ -329,7 +329,7 @@ type WalletDeepLink struct { func (wdl WalletDeepLink) isNativeAsset() bool { return wdl.AssetIssuer == "" && - slices.Contains([]string{"XLM", "NATIVE"}, strings.ToUpper(wdl.AssetCode)) + (wdl.AssetCode == assets.XLMAssetCode || wdl.AssetCode == assets.XLMAssetCodeAlias) } func (wdl WalletDeepLink) assetName() string { diff --git a/internal/services/send_receiver_wallets_invite_service_test.go b/internal/services/send_receiver_wallets_invite_service_test.go index 6e6f9ed0a..f098e2b42 100644 --- a/internal/services/send_receiver_wallets_invite_service_test.go +++ b/internal/services/send_receiver_wallets_invite_service_test.go @@ -1213,10 +1213,10 @@ func Test_WalletDeepLink_isNativeAsset(t *testing.T) { wantResult: true, }, { - name: "🟢 xLm without issuer should be native asset (case insensitive)", - assetCode: "XLM", + name: "🔴 xLm without issuer should NOT be native asset (case sensitive)", + assetCode: "xLm", assetIssuer: "", - wantResult: true, + wantResult: false, }, { name: "🔴 XLM with issuer should NOT be native asset", @@ -1225,16 +1225,16 @@ func Test_WalletDeepLink_isNativeAsset(t *testing.T) { wantResult: false, }, { - name: "🟢 native without issuer should be native asset", - assetCode: "native", + name: "🟢 NATIVE without issuer should be native asset", + assetCode: "NATIVE", assetIssuer: "", wantResult: true, }, { - name: "🟢 NaTiVe without issuer should be native asset (case insensitive)", + name: "🔴 NaTiVe without issuer should NOT be native asset (case sensitive)", assetCode: "NaTiVe", assetIssuer: "", - wantResult: true, + wantResult: false, }, { name: "🔴 native with issuer should NOT be native asset", @@ -1286,7 +1286,7 @@ func Test_WalletDeepLink_assetName(t *testing.T) { name: "'native' native asset", assetCode: "native", assetIssuer: "", - wantResult: "native", + wantResult: "native-", }, { name: "'native' with an issuer", diff --git a/internal/transactionsubmission/services/horizon.go b/internal/transactionsubmission/services/horizon.go index 48227bd99..5ac59489a 100644 --- a/internal/transactionsubmission/services/horizon.go +++ b/internal/transactionsubmission/services/horizon.go @@ -7,7 +7,6 @@ import ( "slices" "sort" "strconv" - "strings" "time" "github.com/avast/retry-go/v4" @@ -410,7 +409,7 @@ func getAccountDetails(client horizonclient.ClientInterface, accountID string) ( return &account, nil } -// getAssetID returns asset identifier formatted as CODE:issuer. +// getAssetID returns asset identifier formatted as code:issuer. func getAssetID(code, issuer string) string { - return fmt.Sprintf("%s:%s", strings.ToUpper(code), issuer) + return fmt.Sprintf("%s:%s", code, issuer) } diff --git a/internal/transactionsubmission/transaction_worker.go b/internal/transactionsubmission/transaction_worker.go index 4028ea77f..f143bc5bf 100644 --- a/internal/transactionsubmission/transaction_worker.go +++ b/internal/transactionsubmission/transaction_worker.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "slices" - "strings" "github.com/google/uuid" "github.com/stellar/go-stellar-sdk/clients/horizonclient" @@ -17,6 +16,7 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/db" "github.com/stellar/stellar-disbursement-platform-backend/internal/crashtracker" sdpMonitor "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor" + "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine" tssMonitor "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/monitor" "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/store" @@ -435,7 +435,7 @@ func (tw *TransactionWorker) buildAndSignTransaction(ctx context.Context, txJob return nil, fmt.Errorf("asset code cannot be empty") } var asset txnbuild.Asset = txnbuild.NativeAsset{} - if strings.ToUpper(txJob.Transaction.AssetCode) != "XLM" { + if txJob.Transaction.AssetCode != assets.XLMAssetCode && txJob.Transaction.AssetCode != assets.XLMAssetCodeAlias { if !strkey.IsValidEd25519PublicKey(txJob.Transaction.AssetIssuer) { return nil, fmt.Errorf("invalid asset issuer: %v", txJob.Transaction.AssetIssuer) } diff --git a/internal/transactionsubmission/transaction_worker_test.go b/internal/transactionsubmission/transaction_worker_test.go index 01df72540..974cd9fb6 100644 --- a/internal/transactionsubmission/transaction_worker_test.go +++ b/internal/transactionsubmission/transaction_worker_test.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "net/http" - "strings" "testing" "github.com/google/uuid" @@ -29,6 +28,7 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/internal/crashtracker" sdpMonitor "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor" "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httpclient" + "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine" engineMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/mocks" "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/preconditions" @@ -1575,7 +1575,7 @@ func Test_TransactionWorker_buildAndSignTransaction(t *testing.T) { // Check that the transaction was built correctly: var wantAsset txnbuild.Asset = txnbuild.NativeAsset{} - if strings.ToUpper(txJob.Transaction.AssetCode) != "XLM" { + if txJob.Transaction.AssetCode != assets.XLMAssetCode && txJob.Transaction.AssetCode != assets.XLMAssetCodeAlias { wantAsset = txnbuild.CreditAsset{ Code: txJob.Transaction.AssetCode, Issuer: txJob.Transaction.AssetIssuer,