From 36c729829199e5df08f70e4317a5c107da3ebe4f Mon Sep 17 00:00:00 2001 From: Denys Pohrebniak Date: Wed, 4 Jun 2025 17:02:03 +0300 Subject: [PATCH 1/4] SDP 1686 create wallet endpoint update --- internal/data/models.go | 2 +- internal/data/wallets.go | 20 +- internal/serve/httphandler/wallets_handler.go | 66 ++-- .../serve/httphandler/wallets_handler_test.go | 271 ++++++++++++- internal/serve/serve.go | 5 +- internal/serve/validators/wallet_validator.go | 136 ++++++- .../serve/validators/wallet_validator_test.go | 355 +++++++++++++++++- internal/services/asset_resolver.go | 78 ++++ internal/services/asset_resolver_test.go | 126 +++++++ 9 files changed, 1007 insertions(+), 52 deletions(-) create mode 100644 internal/services/asset_resolver.go create mode 100644 internal/services/asset_resolver_test.go diff --git a/internal/data/models.go b/internal/data/models.go index 4807204f6..11a55f384 100644 --- a/internal/data/models.go +++ b/internal/data/models.go @@ -39,7 +39,7 @@ func NewModels(dbConnectionPool db.DBConnectionPool) (*Models, error) { } return &Models{ Disbursements: &DisbursementModel{dbConnectionPool: dbConnectionPool}, - Wallets: &WalletModel{dbConnectionPool: dbConnectionPool}, + Wallets: NewWalletModel(dbConnectionPool), Assets: &AssetModel{dbConnectionPool: dbConnectionPool}, Organizations: &OrganizationModel{dbConnectionPool: dbConnectionPool}, Payment: &PaymentModel{dbConnectionPool: dbConnectionPool}, diff --git a/internal/data/wallets.go b/internal/data/wallets.go index bdd1fbbfd..bd048369e 100644 --- a/internal/data/wallets.go +++ b/internal/data/wallets.go @@ -40,6 +40,7 @@ type WalletInsert struct { Homepage string `db:"homepage"` SEP10ClientDomain string `db:"sep_10_client_domain"` DeepLinkSchema string `db:"deep_link_schema"` + Enabled bool `db:"enabled"` AssetsIDs []string `db:"assets_ids"` } @@ -64,6 +65,12 @@ type WalletModel struct { dbConnectionPool db.DBConnectionPool } +func NewWalletModel(dbConnectionPool db.DBConnectionPool) *WalletModel { + return &WalletModel{ + dbConnectionPool: dbConnectionPool, + } +} + // WalletColumnNames returns a comma-separated string of wallet column names for SQL queries. It includes optional date // fields based on the provided parameter. func WalletColumnNames(tableReference, resultAlias string, includeDates bool) string { @@ -163,13 +170,13 @@ func (wm *WalletModel) Insert(ctx context.Context, newWallet WalletInsert) (*Wal const query = ` WITH new_wallet AS ( INSERT INTO wallets - (name, homepage, deep_link_schema, sep_10_client_domain) + (name, homepage, deep_link_schema, sep_10_client_domain, enabled) VALUES - ($1, $2, $3, $4) + ($1, $2, $3, $4, $5) RETURNING * ), assets_cte AS ( - SELECT UNNEST($5::text[]) id + SELECT UNNEST($6::text[]) id ), new_wallet_assets AS ( INSERT INTO wallets_assets (wallet_id, asset_id) @@ -183,12 +190,11 @@ func (wm *WalletModel) Insert(ctx context.Context, newWallet WalletInsert) (*Wal ` var w Wallet - err := dbTx.GetContext( + if err := dbTx.GetContext( ctx, &w, query, - newWallet.Name, newWallet.Homepage, newWallet.DeepLinkSchema, newWallet.SEP10ClientDomain, + newWallet.Name, newWallet.Homepage, newWallet.DeepLinkSchema, newWallet.SEP10ClientDomain, newWallet.Enabled, pq.Array(newWallet.AssetsIDs), - ) - if err != nil { + ); err != nil { if pqError, ok := err.(*pq.Error); ok { constraintErrMap := map[string]error{ "wallets_assets_asset_id_fkey": ErrInvalidAssetID, diff --git a/internal/serve/httphandler/wallets_handler.go b/internal/serve/httphandler/wallets_handler.go index 7721f524b..7433a3b87 100644 --- a/internal/serve/httphandler/wallets_handler.go +++ b/internal/serve/httphandler/wallets_handler.go @@ -1,6 +1,7 @@ package httphandler import ( + "context" "errors" "fmt" "net/http" @@ -12,12 +13,14 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/internal/data" "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/utils" ) type WalletsHandler struct { - Models *data.Models - NetworkType utils.NetworkType + Models *data.Models + NetworkType utils.NetworkType + AssetResolver *services.AssetResolver } // GetWallets returns a list of wallets @@ -74,30 +77,36 @@ func (h WalletsHandler) PostWallets(rw http.ResponseWriter, req *http.Request) { return } - wallet, err := h.Models.Wallets.Insert(ctx, data.WalletInsert{ + // Resolve asset references to IDs + var assetIDs []string + var err error + + if len(reqBody.Assets) > 0 { + assetIDs, err = h.AssetResolver.ResolveAssetReferences(ctx, reqBody.Assets) + if err != nil { + httperror.BadRequest("failed to resolve asset references", err, nil).Render(rw) + return + } + } else if len(reqBody.AssetsIDs) > 0 { + if err = h.AssetResolver.ValidateAssetIDs(ctx, reqBody.AssetsIDs); err != nil { + httperror.BadRequest("invalid asset ID", err, nil).Render(rw) + return + } + assetIDs = reqBody.AssetsIDs + } + + walletInsert := data.WalletInsert{ Name: reqBody.Name, Homepage: reqBody.Homepage, SEP10ClientDomain: reqBody.SEP10ClientDomain, DeepLinkSchema: reqBody.DeepLinkSchema, - AssetsIDs: reqBody.AssetsIDs, - }) - if err != nil { - switch { - case errors.Is(err, data.ErrInvalidAssetID): - httperror.BadRequest(data.ErrInvalidAssetID.Error(), err, nil).Render(rw) - return - case errors.Is(err, data.ErrWalletNameAlreadyExists): - httperror.Conflict(data.ErrWalletNameAlreadyExists.Error(), err, nil).Render(rw) - return - case errors.Is(err, data.ErrWalletHomepageAlreadyExists): - httperror.Conflict(data.ErrWalletHomepageAlreadyExists.Error(), err, nil).Render(rw) - return - case errors.Is(err, data.ErrWalletDeepLinkSchemaAlreadyExists): - httperror.Conflict(data.ErrWalletDeepLinkSchemaAlreadyExists.Error(), err, nil).Render(rw) - return - } + AssetsIDs: assetIDs, + Enabled: *reqBody.Enabled, + } - httperror.InternalError(ctx, "", err, nil).Render(rw) + wallet, err := h.Models.Wallets.Insert(ctx, walletInsert) + if err != nil { + h.handleWalletCreationError(ctx, rw, err) return } @@ -159,3 +168,18 @@ func (h WalletsHandler) PatchWallets(rw http.ResponseWriter, req *http.Request) httpjson.Render(rw, map[string]string{"message": "wallet updated successfully"}, httpjson.JSON) } + +func (h WalletsHandler) handleWalletCreationError(ctx context.Context, rw http.ResponseWriter, err error) { + switch { + case errors.Is(err, data.ErrInvalidAssetID): + httperror.BadRequest(data.ErrInvalidAssetID.Error(), err, nil).Render(rw) + case errors.Is(err, data.ErrWalletNameAlreadyExists): + httperror.Conflict(data.ErrWalletNameAlreadyExists.Error(), err, nil).Render(rw) + case errors.Is(err, data.ErrWalletHomepageAlreadyExists): + httperror.Conflict(data.ErrWalletHomepageAlreadyExists.Error(), err, nil).Render(rw) + case errors.Is(err, data.ErrWalletDeepLinkSchemaAlreadyExists): + httperror.Conflict(data.ErrWalletDeepLinkSchemaAlreadyExists.Error(), err, nil).Render(rw) + default: + httperror.InternalError(ctx, "failed to create wallet", err, nil).Render(rw) + } +} diff --git a/internal/serve/httphandler/wallets_handler_test.go b/internal/serve/httphandler/wallets_handler_test.go index ccbcd3851..08307c9f5 100644 --- a/internal/serve/httphandler/wallets_handler_test.go +++ b/internal/serve/httphandler/wallets_handler_test.go @@ -18,15 +18,13 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/db/dbtest" "github.com/stellar/stellar-disbursement-platform-backend/internal/data" "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httperror" + "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/utils" ) func Test_WalletsHandlerGetWallets(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, outerErr := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, outerErr) - defer dbConnectionPool.Close() + dbConnectionPool := getDBConnectionPool(t) models, outerErr := data.NewModels(dbConnectionPool) require.NoError(t, outerErr) @@ -172,17 +170,14 @@ func Test_WalletsHandlerGetWallets(t *testing.T) { } func Test_WalletsHandlerPostWallets(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() + dbConnectionPool := getDBConnectionPool(t) models, err := data.NewModels(dbConnectionPool) require.NoError(t, err) ctx := context.Background() - handler := &WalletsHandler{Models: models} + assetResolver := services.NewAssetResolver(models.Assets) + handler := &WalletsHandler{Models: models, AssetResolver: assetResolver} // Fixture setup wallet := data.ClearAndCreateWalletFixtures(t, ctx, dbConnectionPool)[0] @@ -212,7 +207,7 @@ func Test_WalletsHandlerPostWallets(t *testing.T) { "homepage": "homepage is required", "deep_link_schema": "deep_link_schema is required", "sep_10_client_domain": "sep_10_client_domain is required", - "assets_ids": "provide at least one asset ID" + "assets": "provide at least one 'assets_ids' or 'assets'" } }`, }, @@ -228,7 +223,7 @@ func Test_WalletsHandlerPostWallets(t *testing.T) { expectedBody: `{ "error": "invalid request body", "extras": { - "assets_ids": "provide at least one asset ID" + "assets": "provide at least one 'assets_ids' or 'assets'" } }`, }, @@ -344,6 +339,254 @@ func Test_WalletsHandlerPostWallets(t *testing.T) { } } +func Test_WalletsHandlerPostWallets_WithNewAssetFormat(t *testing.T) { + dbConnectionPool := getDBConnectionPool(t) + + models, err := data.NewModels(dbConnectionPool) + require.NoError(t, err) + + ctx := context.Background() + + assetResolver := services.NewAssetResolver(models.Assets) + + handler := &WalletsHandler{ + Models: models, + NetworkType: utils.PubnetNetworkType, + AssetResolver: assetResolver, + } + + xlm := data.CreateAssetFixture(t, ctx, dbConnectionPool, assets.XLMAssetCode, "") + usdc, err := models.Assets.GetOrCreate(ctx, assets.USDCAssetCode, "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5") + require.NoError(t, err) + + testCases := []struct { + name string + payload string + expectedStatus int + expectedBody string + validateResult func(t *testing.T, wallet *data.Wallet) + }{ + { + name: "🟢 successfully creates wallet with new assets format - ID reference", + payload: fmt.Sprintf(`{ + "name": "New Format Wallet ID", + "homepage": "https://newformat-id.com", + "deep_link_schema": "newformat-id://sdp", + "sep_10_client_domain": "newformat-id.com", + "assets": [ + {"id": %q}, + {"id": %q} + ] + }`, xlm.ID, usdc.ID), + expectedStatus: http.StatusCreated, + validateResult: func(t *testing.T, wallet *data.Wallet) { + assert.Equal(t, "New Format Wallet ID", wallet.Name) + assert.Len(t, wallet.Assets, 2) + + assetCodes := []string{wallet.Assets[0].Code, wallet.Assets[1].Code} + assert.Contains(t, assetCodes, assets.XLMAssetCode) + assert.Contains(t, assetCodes, assets.USDCAssetCode) + }, + }, + { + name: "🟢 successfully creates wallet with native asset reference", + payload: `{ + "name": "Native Asset Wallet", + "homepage": "https://native-wallet.com", + "deep_link_schema": "native://sdp", + "sep_10_client_domain": "native-wallet.com", + "assets": [ + {"type": "native"} + ] + }`, + expectedStatus: http.StatusCreated, + validateResult: func(t *testing.T, wallet *data.Wallet) { + assert.Len(t, wallet.Assets, 1) + assert.Equal(t, assets.XLMAssetCode, wallet.Assets[0].Code) + assert.Equal(t, "", wallet.Assets[0].Issuer) + }, + }, + { + name: "🟢 successfully creates wallet with classic asset reference", + payload: `{ + "name": "Classic Asset Wallet", + "homepage": "https://classic-wallet.com", + "deep_link_schema": "classic://sdp", + "sep_10_client_domain": "classic-wallet.com", + "assets": [ + { + "type": "classic", + "code": "USDC", + "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + } + ] + }`, + expectedStatus: http.StatusCreated, + validateResult: func(t *testing.T, wallet *data.Wallet) { + assert.Len(t, wallet.Assets, 1) + assert.Equal(t, assets.USDCAssetCode, wallet.Assets[0].Code) + assert.Equal(t, "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", wallet.Assets[0].Issuer) + }, + }, + { + name: "🟢 successfully creates wallet with mixed asset references", + payload: fmt.Sprintf(`{ + "name": "Mixed Assets Wallet", + "homepage": "https://mixed-wallet.com", + "deep_link_schema": "mixed://sdp", + "sep_10_client_domain": "mixed-wallet.com", + "assets": [ + {"id": %q}, + {"type": "native"} + ] + }`, usdc.ID), + expectedStatus: http.StatusCreated, + validateResult: func(t *testing.T, wallet *data.Wallet) { + assert.Len(t, wallet.Assets, 2) + + assetMap := make(map[string]string) + for _, asset := range wallet.Assets { + assetMap[asset.Code] = asset.Issuer + } + + assert.Equal(t, "", assetMap[assets.XLMAssetCode]) + assert.Equal(t, "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", assetMap["USDC"]) + }, + }, + { + name: "🟢 successfully creates wallet with enabled=false", + payload: fmt.Sprintf(`{ + "name": "Disabled Wallet", + "homepage": "https://disabled-wallet.com", + "deep_link_schema": "disabled://sdp", + "sep_10_client_domain": "disabled-wallet.com", + "assets": [{"id": %q}], + "enabled": false + }`, xlm.ID), + expectedStatus: http.StatusCreated, + validateResult: func(t *testing.T, wallet *data.Wallet) { + assert.False(t, wallet.Enabled) + }, + }, + { + name: "🔴 fails when mixing assets_ids and assets", + payload: fmt.Sprintf(`{ + "name": "Mixed Format Wallet", + "homepage": "https://mixed-format.com", + "deep_link_schema": "mixed-format://sdp", + "sep_10_client_domain": "mixed-format.com", + "assets_ids": [%q], + "assets": [{"type": "native"}] + }`, xlm.ID), + expectedStatus: http.StatusBadRequest, + expectedBody: `{ + "error": "invalid request body", + "extras": { + "assets": "cannot use both 'assets_ids' and 'assets' fields simultaneously" + } + }`, + }, + { + name: "🔴 fails with invalid asset reference", + payload: `{ + "name": "Invalid Asset Wallet", + "homepage": "https://invalid-asset.com", + "deep_link_schema": "invalid-asset://sdp", + "sep_10_client_domain": "invalid-asset.com", + "assets": [ + {"type": "classic", "code": "MISSING_ISSUER"} + ] + }`, + expectedStatus: http.StatusBadRequest, + expectedBody: `{ + "error": "invalid request body", + "extras": { + "assets[0]": "'issuer' is required for classic asset" + } + }`, + }, + { + name: "🔴 fails with non-existent asset ID", + payload: `{ + "name": "Non-existent Asset Wallet", + "homepage": "https://nonexistent.com", + "deep_link_schema": "nonexistent://sdp", + "sep_10_client_domain": "nonexistent.com", + "assets": [ + {"id": "non-existent-id"} + ] + }`, + expectedStatus: http.StatusBadRequest, + expectedBody: `{"error": "failed to resolve asset references"}`, + }, + { + name: "🔴 fails with contract asset (not implemented)", + payload: `{ + "name": "Contract Asset Wallet", + "homepage": "https://contract.com", + "deep_link_schema": "contract://sdp", + "sep_10_client_domain": "contract.com", + "assets": [ + {"type": "contract", "code": "USDC", "contract_id": "CA..."} + ] + }`, + expectedStatus: http.StatusBadRequest, + expectedBody: `{ + "error": "invalid request body", + "extras": { + "assets[0]": "contract assets are not implemented yet" + } + }`, + }, + { + name: "🔴 fails with fiat asset (not implemented)", + payload: `{ + "name": "Fiat Asset Wallet", + "homepage": "https://fiat.com", + "deep_link_schema": "fiat://sdp", + "sep_10_client_domain": "fiat.com", + "assets": [ + {"type": "fiat", "code": "USD"} + ] + }`, + expectedStatus: http.StatusBadRequest, + expectedBody: `{ + "error": "invalid request body", + "extras": { + "assets[0]": "fiat assets are not implemented yet" + } + }`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rr := httptest.NewRecorder() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(tc.payload)) + require.NoError(t, err) + + http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req) + + resp := rr.Result() + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + assert.Equal(t, tc.expectedStatus, resp.StatusCode) + + if tc.expectedBody != "" { + assert.JSONEq(t, tc.expectedBody, string(respBody)) + } else if tc.expectedStatus == http.StatusCreated && tc.validateResult != nil { + var wallet data.Wallet + err = json.Unmarshal(respBody, &wallet) + require.NoError(t, err) + + tc.validateResult(t, &wallet) + } + }) + } +} + func Test_WalletsHandlerDeleteWallet(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() diff --git a/internal/serve/serve.go b/internal/serve/serve.go index ec5336e91..c4d8ccb0d 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -374,8 +374,9 @@ func handleHTTP(o ServeOptions) *chi.Mux { r.With(middleware.AnyRoleMiddleware(authManager, data.GetAllRoles()...)).Route("/wallets", func(r chi.Router) { walletsHandler := httphandler.WalletsHandler{ - Models: o.Models, - NetworkType: o.NetworkType, + Models: o.Models, + NetworkType: o.NetworkType, + AssetResolver: services.NewAssetResolver(o.Models.Assets), } r.Get("/", walletsHandler.GetWallets) r.With(middleware.AnyRoleMiddleware(authManager, data.DeveloperUserRole)). diff --git a/internal/serve/validators/wallet_validator.go b/internal/serve/validators/wallet_validator.go index dcd34239d..cc7593eac 100644 --- a/internal/serve/validators/wallet_validator.go +++ b/internal/serve/validators/wallet_validator.go @@ -2,20 +2,45 @@ package validators import ( "context" + "fmt" "net/url" "strings" + "github.com/stellar/go/strkey" "github.com/stellar/go/support/log" + "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" ) +type AssetReferenceType string + +const ( + AssetReferenceTypeID AssetReferenceType = "id" + AssetReferenceTypeClassic AssetReferenceType = "classic" + AssetReferenceTypeNative AssetReferenceType = "native" + AssetReferenceTypeContract AssetReferenceType = "contract" + AssetReferenceTypeFiat AssetReferenceType = "fiat" +) + +type AssetReference struct { + // For ID-based reference + ID string `json:"id,omitempty"` + + Type string `json:"type,omitempty"` + Code string `json:"code,omitempty"` + Issuer string `json:"issuer,omitempty"` + ContractID string `json:"contract_id,omitempty"` +} + type WalletRequest struct { - Name string `json:"name"` - Homepage string `json:"homepage"` - DeepLinkSchema string `json:"deep_link_schema"` - SEP10ClientDomain string `json:"sep_10_client_domain"` - AssetsIDs []string `json:"assets_ids"` + Name string `json:"name"` + Homepage string `json:"homepage"` + DeepLinkSchema string `json:"deep_link_schema"` + SEP10ClientDomain string `json:"sep_10_client_domain"` + Enabled *bool `json:"enabled,omitempty"` + Assets []AssetReference `json:"assets,omitempty"` + AssetsIDs []string `json:"assets_ids,omitempty"` // Legacy support } type PatchWalletRequest struct { @@ -47,7 +72,20 @@ func (wv *WalletValidator) ValidateCreateWalletRequest(ctx context.Context, reqB wv.Check(homepage != "", "homepage", "homepage is required") wv.Check(deepLinkSchema != "", "deep_link_schema", "deep_link_schema is required") wv.Check(sep10ClientDomain != "", "sep_10_client_domain", "sep_10_client_domain is required") - wv.Check(len(reqBody.AssetsIDs) != 0, "assets_ids", "provide at least one asset ID") + wv.Check(len(reqBody.AssetsIDs) != 0 || len(reqBody.Assets) != 0, "assets", "provide at least one 'assets_ids' or 'assets'") + wv.Check(len(reqBody.AssetsIDs) == 0 || len(reqBody.Assets) == 0, "assets", "cannot use both 'assets_ids' and 'assets' fields simultaneously") + var processedAssets []AssetReference + if len(reqBody.Assets) != 0 { + for i, asset := range reqBody.Assets { + inferredAsset := wv.inferAssetType(asset) + if err := inferredAsset.Validate(); err != nil { + wv.Check(false, fmt.Sprintf("assets[%d]", i), err.Error()) + continue + } + processedAssets = append(processedAssets, inferredAsset) + } + } + if wv.HasErrors() { return nil } @@ -86,6 +124,11 @@ func (wv *WalletValidator) ValidateCreateWalletRequest(ctx context.Context, reqB wv.Check(false, "sep_10_client_domain", "invalid SEP-10 client domain provided") } + if reqBody.Enabled == nil { + defEnabled := true + reqBody.Enabled = &defEnabled + } + if wv.HasErrors() { return nil } @@ -95,7 +138,9 @@ func (wv *WalletValidator) ValidateCreateWalletRequest(ctx context.Context, reqB Homepage: homepageURL.String(), DeepLinkSchema: deepLinkSchemaURL.String(), SEP10ClientDomain: sep10Host, + Assets: processedAssets, AssetsIDs: reqBody.AssetsIDs, + Enabled: reqBody.Enabled, } return modifiedReq @@ -108,3 +153,82 @@ func (wv *WalletValidator) ValidatePatchWalletRequest(reqBody *PatchWalletReques } wv.Check(reqBody.Enabled != nil, "enabled", "enabled is required") } + +func (ar AssetReference) Validate() error { + if ar.ID != "" { + if ar.Type != "" || ar.Code != "" || ar.Issuer != "" || ar.ContractID != "" { + return fmt.Errorf("when 'id' is provided, other fields should not be present") + } + return nil + } + + if ar.Type == "" { + return fmt.Errorf("either 'id' or 'type' must be provided") + } + + switch AssetReferenceType(ar.Type) { + case AssetReferenceTypeClassic: + if ar.Code == "" { + return fmt.Errorf("'code' is required for classic asset") + } + if ar.Issuer == "" { + return fmt.Errorf("'issuer' is required for classic asset") + } + + if !strkey.IsValidEd25519PublicKey(ar.Issuer) { + return fmt.Errorf("invalid issuer address format") + } + + case AssetReferenceTypeNative: + if ar.Code != "" || ar.Issuer != "" || ar.ContractID != "" { + return fmt.Errorf("native asset should not have code, issuer, or contract_id") + } + + case AssetReferenceTypeContract: + return fmt.Errorf("contract assets are not implemented yet") + + case AssetReferenceTypeFiat: + return fmt.Errorf("fiat assets are not implemented yet") + + default: + return fmt.Errorf("invalid asset type: %s", ar.Type) + } + + return nil +} + +func (ar AssetReference) GetReferenceType() AssetReferenceType { + if ar.ID != "" { + return AssetReferenceTypeID + } + return AssetReferenceType(ar.Type) +} + +func (wv *WalletValidator) inferAssetType(asset AssetReference) AssetReference { + // If ID is provided, no inference needed + if asset.ID != "" { + return asset + } + + // If type is already specified, no inference needed + if asset.Type != "" { + return asset + } + + // Inference logic for backward compatibility + result := asset + + if strings.ToUpper(asset.Code) == assets.XLMAssetCode && asset.Issuer == "" { + result.Type = string(AssetReferenceTypeNative) + result.Code = "" + return result + } + + // Classic asset detection: has both code and issuer + if asset.Code != "" && asset.Issuer != "" { + result.Type = string(AssetReferenceTypeClassic) + return result + } + + return result +} diff --git a/internal/serve/validators/wallet_validator_test.go b/internal/serve/validators/wallet_validator_test.go index fcaf71201..c764e13da 100644 --- a/internal/serve/validators/wallet_validator_test.go +++ b/internal/serve/validators/wallet_validator_test.go @@ -9,6 +9,7 @@ import ( ) func TestWalletValidator_ValidateCreateWalletRequest(t *testing.T) { + t.Parallel() ctx := context.Background() testCases := []struct { @@ -31,7 +32,7 @@ func TestWalletValidator_ValidateCreateWalletRequest(t *testing.T) { "homepage": "homepage is required", "name": "name is required", "sep_10_client_domain": "sep_10_client_domain is required", - "assets_ids": "provide at least one asset ID", + "assets": "provide at least one 'assets_ids' or 'assets'", }, }, { @@ -101,7 +102,9 @@ func TestWalletValidator_ValidateCreateWalletRequest(t *testing.T) { } for _, tc := range testCases { + tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() wv := NewWalletValidator() reqBody := wv.ValidateCreateWalletRequest(ctx, tc.reqBody, tc.enforceHTTPS) @@ -120,9 +123,11 @@ func TestWalletValidator_ValidateCreateWalletRequest(t *testing.T) { } func TestWalletValidator_ValidatePatchWalletRequest(t *testing.T) { + t.Parallel() ctx := context.Background() t.Run("returns error when request body is empty", func(t *testing.T) { + t.Parallel() wv := NewWalletValidator() wv.ValidateCreateWalletRequest(ctx, nil, false) assert.True(t, wv.HasErrors()) @@ -130,6 +135,7 @@ func TestWalletValidator_ValidatePatchWalletRequest(t *testing.T) { }) t.Run("returns error when body has empty fields", func(t *testing.T) { + t.Parallel() wv := NewWalletValidator() reqBody := &PatchWalletRequest{} @@ -141,6 +147,7 @@ func TestWalletValidator_ValidatePatchWalletRequest(t *testing.T) { }) t.Run("validates successfully", func(t *testing.T) { + t.Parallel() wv := NewWalletValidator() e := new(bool) @@ -164,3 +171,349 @@ func TestWalletValidator_ValidatePatchWalletRequest(t *testing.T) { assert.Empty(t, wv.Errors) }) } + +func TestAssetReference_Validate(t *testing.T) { + testCases := []struct { + name string + assetRef AssetReference + expectedError string + }{ + { + name: "valid ID reference", + assetRef: AssetReference{ + ID: "ef262966-1cbb-4fdb-9f6f-cc335e954dd1", + }, + expectedError: "", + }, + { + name: "ID reference with other fields should fail", + assetRef: AssetReference{ + ID: "ef262966-1cbb-4fdb-9f6f-cc335e954dd1", + Type: "classic", + Code: "USDC", + }, + expectedError: "when 'id' is provided, other fields should not be present", + }, + + { + name: "valid classic asset", + assetRef: AssetReference{ + Type: "classic", + Code: "USDC", + Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + }, + expectedError: "", + }, + { + name: "classic asset missing code", + assetRef: AssetReference{ + Type: "classic", + Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + }, + expectedError: "'code' is required for classic asset", + }, + { + name: "classic asset missing issuer", + assetRef: AssetReference{ + Type: "classic", + Code: "USDC", + }, + expectedError: "'issuer' is required for classic asset", + }, + { + name: "classic asset with invalid issuer format", + assetRef: AssetReference{ + Type: "classic", + Code: "USDC", + Issuer: "invalid-issuer", + }, + expectedError: "invalid issuer address format", + }, + { + name: "classic asset with issuer starting with wrong character", + assetRef: AssetReference{ + Type: "classic", + Code: "USDC", + Issuer: "ABBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + }, + expectedError: "invalid issuer address format", + }, + + { + name: "valid native asset", + assetRef: AssetReference{ + Type: "native", + }, + expectedError: "", + }, + { + name: "native asset with code should fail", + assetRef: AssetReference{ + Type: "native", + Code: "XLM", + }, + expectedError: "native asset should not have code, issuer, or contract_id", + }, + { + name: "native asset with issuer should fail", + assetRef: AssetReference{ + Type: "native", + Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + }, + expectedError: "native asset should not have code, issuer, or contract_id", + }, + + // Invalid cases + { + name: "empty reference", + assetRef: AssetReference{}, + expectedError: "either 'id' or 'type' must be provided", + }, + { + name: "invalid asset type", + assetRef: AssetReference{ + Type: "invalid-type", + }, + expectedError: "invalid asset type: invalid-type", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.assetRef.Validate() + if tc.expectedError == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedError) + } + }) + } +} + +func TestWalletValidator_ValidateCreateWalletRequest_WithAssets(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + reqBody *WalletRequest + enforceHTTPS bool + expectedError map[string]string + expectedBody *WalletRequest + }{ + { + name: "valid request with legacy assets_ids", + reqBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + AssetsIDs: []string{"asset-id-1", "asset-id-2"}, + }, + enforceHTTPS: true, + expectedError: nil, + expectedBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + AssetsIDs: []string{"asset-id-1", "asset-id-2"}, + Assets: nil, + Enabled: boolToPtr(true), + }, + }, + { + name: "valid request with new assets format", + reqBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + Assets: []AssetReference{ + {ID: "asset-id-1"}, + {Type: "native"}, + {Type: "classic", Code: "USDC", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, + }, + }, + enforceHTTPS: true, + expectedError: nil, + expectedBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + AssetsIDs: nil, + Assets: []AssetReference{ + {ID: "asset-id-1"}, + {Type: "native"}, + {Type: "classic", Code: "USDC", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, + }, + Enabled: boolToPtr(true), + }, + }, + { + name: "request with enabled field", + reqBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + AssetsIDs: []string{"asset-id-1"}, + Enabled: &[]bool{true}[0], + }, + enforceHTTPS: true, + expectedError: nil, + expectedBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + AssetsIDs: []string{"asset-id-1"}, + Assets: nil, + Enabled: &[]bool{true}[0], + }, + }, + { + name: "error when both assets_ids and assets are provided", + reqBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + AssetsIDs: []string{"asset-id-1"}, + Assets: []AssetReference{ + {ID: "asset-id-2"}, + }, + }, + enforceHTTPS: true, + expectedError: map[string]string{ + "assets": "cannot use both 'assets_ids' and 'assets' fields simultaneously", + }, + expectedBody: nil, + }, + { + name: "error when no assets provided", + reqBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + }, + enforceHTTPS: true, + expectedError: map[string]string{ + "assets": "provide at least one 'assets_ids' or 'assets'", + }, + expectedBody: nil, + }, + { + name: "error with invalid asset reference", + reqBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + Assets: []AssetReference{ + {Type: "classic", Code: "USDC"}, + }, + }, + enforceHTTPS: true, + expectedError: map[string]string{ + "assets[0]": "'issuer' is required for classic asset", + }, + expectedBody: nil, + }, + { + name: "error with multiple invalid asset references", + reqBody: &WalletRequest{ + Name: "Test Wallet", + Homepage: "https://testwallet.com", + DeepLinkSchema: "testwallet://sdp", + SEP10ClientDomain: "testwallet.com", + Assets: []AssetReference{ + {Type: "contract", Code: "USDC", ContractID: "CA..."}, + {Type: "fiat", Code: "USD"}, + }, + }, + enforceHTTPS: true, + expectedError: map[string]string{ + "assets[0]": "contract assets are not implemented yet", + "assets[1]": "fiat assets are not implemented yet", + }, + expectedBody: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + validator := NewWalletValidator() + result := validator.ValidateCreateWalletRequest(ctx, tc.reqBody, tc.enforceHTTPS) + + if tc.expectedError != nil { + require.True(t, validator.HasErrors()) + require.Nil(t, result) + + for field, expectedMsg := range tc.expectedError { + actualErrors, ok := validator.Errors[field] + require.True(t, ok, "Expected error for field %s not found", field) + require.Contains(t, actualErrors, expectedMsg) + } + } else { + require.False(t, validator.HasErrors()) + require.NotNil(t, result) + assert.Equal(t, tc.expectedBody, result) + } + }) + } +} + +func TestWalletValidator_BackwardCompatibility(t *testing.T) { + ctx := context.Background() + + legacyRequest := &WalletRequest{ + Name: "Legacy Wallet", + Homepage: "https://legacy.com", + DeepLinkSchema: "legacy://sdp", + SEP10ClientDomain: "legacy.com", + AssetsIDs: []string{"id-1", "id-2", "id-3"}, + } + + validator := NewWalletValidator() + result := validator.ValidateCreateWalletRequest(ctx, legacyRequest, true) + + require.False(t, validator.HasErrors()) + require.NotNil(t, result) + assert.Equal(t, []string{"id-1", "id-2", "id-3"}, result.AssetsIDs) + assert.Nil(t, result.Assets) +} + +func TestWalletValidator_AssetReferenceValidation(t *testing.T) { + ctx := context.Background() + + request := &WalletRequest{ + Name: "Multi Asset Wallet", + Homepage: "https://multi.com", + DeepLinkSchema: "multi://sdp", + SEP10ClientDomain: "multi.com", + Assets: []AssetReference{ + {ID: "existing-id"}, + {Type: "native"}, + {Type: "classic", Code: "USDC", Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"}, + + {Type: "classic", Code: "BAD"}, + }, + } + + validator := NewWalletValidator() + result := validator.ValidateCreateWalletRequest(ctx, request, true) + + require.True(t, validator.HasErrors()) + require.Nil(t, result) + + errors, ok := validator.Errors["assets[3]"] + require.True(t, ok) + assert.Contains(t, errors, "'issuer' is required for classic asset") +} + +func boolToPtr(b bool) *bool { + return &b +} diff --git a/internal/services/asset_resolver.go b/internal/services/asset_resolver.go new file mode 100644 index 000000000..149ddac90 --- /dev/null +++ b/internal/services/asset_resolver.go @@ -0,0 +1,78 @@ +package services + +import ( + "context" + "fmt" + + "github.com/stellar/stellar-disbursement-platform-backend/internal/data" + "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/validators" +) + +type AssetResolver struct { + assetModel *data.AssetModel +} + +func NewAssetResolver(assetModel *data.AssetModel) *AssetResolver { + return &AssetResolver{ + assetModel: assetModel, + } +} + +func (ar *AssetResolver) ResolveAssetReferences(ctx context.Context, references []validators.AssetReference) ([]string, error) { + assetIDs := make([]string, 0, len(references)) + + for i, ref := range references { + assetID, err := ar.resolveAssetReference(ctx, ref) + if err != nil { + return nil, fmt.Errorf("failed to resolve asset reference at index %d: %w", i, err) + } + assetIDs = append(assetIDs, assetID) + } + + return assetIDs, nil +} + +func (ar *AssetResolver) resolveAssetReference(ctx context.Context, ref validators.AssetReference) (string, error) { + switch ref.GetReferenceType() { + case validators.AssetReferenceTypeID: + asset, err := ar.assetModel.Get(ctx, ref.ID) + if err != nil { + if err == data.ErrRecordNotFound { + return "", fmt.Errorf("asset with ID %s not found", ref.ID) + } + return "", fmt.Errorf("failed to get asset by ID: %w", err) + } + return asset.ID, nil + case validators.AssetReferenceTypeClassic: + asset, err := ar.assetModel.GetOrCreate(ctx, ref.Code, ref.Issuer) + if err != nil { + return "", fmt.Errorf("failed to get or create classic asset: %w", err) + } + return asset.ID, nil + case validators.AssetReferenceTypeNative: + asset, err := ar.assetModel.GetOrCreate(ctx, "XLM", "") + if err != nil { + return "", fmt.Errorf("failed to get or create native asset: %w", err) + } + return asset.ID, nil + case validators.AssetReferenceTypeContract: + return "", fmt.Errorf("contract assets are not implemented yet") + case validators.AssetReferenceTypeFiat: + return "", fmt.Errorf("fiat assets are not implemented yet") + default: + return "", fmt.Errorf("unknown asset reference type") + } +} + +func (ar *AssetResolver) ValidateAssetIDs(ctx context.Context, assetIDs []string) error { + for _, assetID := range assetIDs { + _, err := ar.assetModel.Get(ctx, assetID) + if err != nil { + if err == data.ErrRecordNotFound { + return fmt.Errorf("asset with ID %s not found", assetID) + } + return fmt.Errorf("failed to validate asset ID %s: %w", assetID, err) + } + } + return nil +} diff --git a/internal/services/asset_resolver_test.go b/internal/services/asset_resolver_test.go new file mode 100644 index 000000000..d2e569c41 --- /dev/null +++ b/internal/services/asset_resolver_test.go @@ -0,0 +1,126 @@ +package services + +import ( + "context" + "reflect" + "testing" + + "github.com/stellar/stellar-disbursement-platform-backend/internal/data" + "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/validators" +) + +func TestNewAssetResolver(t *testing.T) { + type args struct { + assetModel *data.AssetModel + } + tests := []struct { + name string + args args + want *AssetResolver + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewAssetResolver(tt.args.assetModel); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewAssetResolver() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAssetResolver_ResolveAssetReferences(t *testing.T) { + type fields struct { + assetModel *data.AssetModel + } + type args struct { + ctx context.Context + references []validators.AssetReference + } + tests := []struct { + name string + fields fields + args args + want []string + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ar := &AssetResolver{ + assetModel: tt.fields.assetModel, + } + got, err := ar.ResolveAssetReferences(tt.args.ctx, tt.args.references) + if (err != nil) != tt.wantErr { + t.Errorf("AssetResolver.ResolveAssetReferences() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AssetResolver.ResolveAssetReferences() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAssetResolver_resolveAssetReference(t *testing.T) { + type fields struct { + assetModel *data.AssetModel + } + type args struct { + ctx context.Context + ref validators.AssetReference + } + tests := []struct { + name string + fields fields + args args + want string + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ar := &AssetResolver{ + assetModel: tt.fields.assetModel, + } + got, err := ar.resolveAssetReference(tt.args.ctx, tt.args.ref) + if (err != nil) != tt.wantErr { + t.Errorf("AssetResolver.resolveAssetReference() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("AssetResolver.resolveAssetReference() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAssetResolver_ValidateAssetIDs(t *testing.T) { + type fields struct { + assetModel *data.AssetModel + } + type args struct { + ctx context.Context + assetIDs []string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ar := &AssetResolver{ + assetModel: tt.fields.assetModel, + } + if err := ar.ValidateAssetIDs(tt.args.ctx, tt.args.assetIDs); (err != nil) != tt.wantErr { + t.Errorf("AssetResolver.ValidateAssetIDs() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From d21136624be15638f067d7cbc46f6f274f5c02ff Mon Sep 17 00:00:00 2001 From: Denys Pohrebniak Date: Wed, 4 Jun 2025 18:10:21 +0300 Subject: [PATCH 2/4] SDP-1686 --- internal/data/models.go | 2 +- internal/data/wallets.go | 6 -- internal/serve/validators/wallet_validator.go | 68 +++++++++---------- internal/services/asset_resolver.go | 6 +- 4 files changed, 34 insertions(+), 48 deletions(-) diff --git a/internal/data/models.go b/internal/data/models.go index 11a55f384..4807204f6 100644 --- a/internal/data/models.go +++ b/internal/data/models.go @@ -39,7 +39,7 @@ func NewModels(dbConnectionPool db.DBConnectionPool) (*Models, error) { } return &Models{ Disbursements: &DisbursementModel{dbConnectionPool: dbConnectionPool}, - Wallets: NewWalletModel(dbConnectionPool), + Wallets: &WalletModel{dbConnectionPool: dbConnectionPool}, Assets: &AssetModel{dbConnectionPool: dbConnectionPool}, Organizations: &OrganizationModel{dbConnectionPool: dbConnectionPool}, Payment: &PaymentModel{dbConnectionPool: dbConnectionPool}, diff --git a/internal/data/wallets.go b/internal/data/wallets.go index bd048369e..84e3cce04 100644 --- a/internal/data/wallets.go +++ b/internal/data/wallets.go @@ -65,12 +65,6 @@ type WalletModel struct { dbConnectionPool db.DBConnectionPool } -func NewWalletModel(dbConnectionPool db.DBConnectionPool) *WalletModel { - return &WalletModel{ - dbConnectionPool: dbConnectionPool, - } -} - // WalletColumnNames returns a comma-separated string of wallet column names for SQL queries. It includes optional date // fields based on the provided parameter. func WalletColumnNames(tableReference, resultAlias string, includeDates bool) string { diff --git a/internal/serve/validators/wallet_validator.go b/internal/serve/validators/wallet_validator.go index cc7593eac..20f5d0651 100644 --- a/internal/serve/validators/wallet_validator.go +++ b/internal/serve/validators/wallet_validator.go @@ -146,6 +146,35 @@ func (wv *WalletValidator) ValidateCreateWalletRequest(ctx context.Context, reqB return modifiedReq } +func (wv *WalletValidator) inferAssetType(asset AssetReference) AssetReference { + // If ID is provided, no inference needed + if asset.ID != "" { + return asset + } + + // If type is already specified, no inference needed + if asset.Type != "" { + return asset + } + + // Inference logic for backward compatibility + result := asset + + if strings.ToUpper(asset.Code) == assets.XLMAssetCode && asset.Issuer == "" { + result.Type = string(AssetReferenceTypeNative) + result.Code = "" + return result + } + + // Classic asset detection: has both code and issuer + if asset.Code != "" && asset.Issuer != "" { + result.Type = string(AssetReferenceTypeClassic) + return result + } + + return result +} + func (wv *WalletValidator) ValidatePatchWalletRequest(reqBody *PatchWalletRequest) { wv.Check(reqBody != nil, "body", "request body is empty") if wv.HasErrors() { @@ -178,18 +207,12 @@ func (ar AssetReference) Validate() error { if !strkey.IsValidEd25519PublicKey(ar.Issuer) { return fmt.Errorf("invalid issuer address format") } - case AssetReferenceTypeNative: if ar.Code != "" || ar.Issuer != "" || ar.ContractID != "" { return fmt.Errorf("native asset should not have code, issuer, or contract_id") } - - case AssetReferenceTypeContract: - return fmt.Errorf("contract assets are not implemented yet") - - case AssetReferenceTypeFiat: - return fmt.Errorf("fiat assets are not implemented yet") - + case AssetReferenceTypeContract, AssetReferenceTypeFiat: + return fmt.Errorf("assets are not implemented yet") default: return fmt.Errorf("invalid asset type: %s", ar.Type) } @@ -203,32 +226,3 @@ func (ar AssetReference) GetReferenceType() AssetReferenceType { } return AssetReferenceType(ar.Type) } - -func (wv *WalletValidator) inferAssetType(asset AssetReference) AssetReference { - // If ID is provided, no inference needed - if asset.ID != "" { - return asset - } - - // If type is already specified, no inference needed - if asset.Type != "" { - return asset - } - - // Inference logic for backward compatibility - result := asset - - if strings.ToUpper(asset.Code) == assets.XLMAssetCode && asset.Issuer == "" { - result.Type = string(AssetReferenceTypeNative) - result.Code = "" - return result - } - - // Classic asset detection: has both code and issuer - if asset.Code != "" && asset.Issuer != "" { - result.Type = string(AssetReferenceTypeClassic) - return result - } - - return result -} diff --git a/internal/services/asset_resolver.go b/internal/services/asset_resolver.go index 149ddac90..cb15c47f3 100644 --- a/internal/services/asset_resolver.go +++ b/internal/services/asset_resolver.go @@ -55,10 +55,8 @@ func (ar *AssetResolver) resolveAssetReference(ctx context.Context, ref validato return "", fmt.Errorf("failed to get or create native asset: %w", err) } return asset.ID, nil - case validators.AssetReferenceTypeContract: - return "", fmt.Errorf("contract assets are not implemented yet") - case validators.AssetReferenceTypeFiat: - return "", fmt.Errorf("fiat assets are not implemented yet") + case validators.AssetReferenceTypeContract, validators.AssetReferenceTypeFiat: + return "", fmt.Errorf("assets are not implemented yet") default: return "", fmt.Errorf("unknown asset reference type") } From a54f6a06f8fc57c8e67b78db891c09bcf7d7e0aa Mon Sep 17 00:00:00 2001 From: Denys Pohrebniak Date: Wed, 4 Jun 2025 18:21:54 +0300 Subject: [PATCH 3/4] SDP-1686 fix test --- .../serve/httphandler/wallets_handler_test.go | 21 +------------------ .../serve/validators/wallet_validator_test.go | 4 ++-- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/internal/serve/httphandler/wallets_handler_test.go b/internal/serve/httphandler/wallets_handler_test.go index 08307c9f5..632cb103d 100644 --- a/internal/serve/httphandler/wallets_handler_test.go +++ b/internal/serve/httphandler/wallets_handler_test.go @@ -534,26 +534,7 @@ func Test_WalletsHandlerPostWallets_WithNewAssetFormat(t *testing.T) { expectedBody: `{ "error": "invalid request body", "extras": { - "assets[0]": "contract assets are not implemented yet" - } - }`, - }, - { - name: "🔴 fails with fiat asset (not implemented)", - payload: `{ - "name": "Fiat Asset Wallet", - "homepage": "https://fiat.com", - "deep_link_schema": "fiat://sdp", - "sep_10_client_domain": "fiat.com", - "assets": [ - {"type": "fiat", "code": "USD"} - ] - }`, - expectedStatus: http.StatusBadRequest, - expectedBody: `{ - "error": "invalid request body", - "extras": { - "assets[0]": "fiat assets are not implemented yet" + "assets[0]": "assets are not implemented yet" } }`, }, diff --git a/internal/serve/validators/wallet_validator_test.go b/internal/serve/validators/wallet_validator_test.go index c764e13da..62761a39f 100644 --- a/internal/serve/validators/wallet_validator_test.go +++ b/internal/serve/validators/wallet_validator_test.go @@ -436,8 +436,8 @@ func TestWalletValidator_ValidateCreateWalletRequest_WithAssets(t *testing.T) { }, enforceHTTPS: true, expectedError: map[string]string{ - "assets[0]": "contract assets are not implemented yet", - "assets[1]": "fiat assets are not implemented yet", + "assets[0]": "assets are not implemented yet", + "assets[1]": "assets are not implemented yet", }, expectedBody: nil, }, From 081f77a59bbc201da3ec6c6b63379b558286c4d6 Mon Sep 17 00:00:00 2001 From: Denys Pohrebniak Date: Thu, 5 Jun 2025 16:08:33 +0300 Subject: [PATCH 4/4] SDP-1686 --- internal/services/asset_resolver_test.go | 126 ----------------------- 1 file changed, 126 deletions(-) delete mode 100644 internal/services/asset_resolver_test.go diff --git a/internal/services/asset_resolver_test.go b/internal/services/asset_resolver_test.go deleted file mode 100644 index d2e569c41..000000000 --- a/internal/services/asset_resolver_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package services - -import ( - "context" - "reflect" - "testing" - - "github.com/stellar/stellar-disbursement-platform-backend/internal/data" - "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/validators" -) - -func TestNewAssetResolver(t *testing.T) { - type args struct { - assetModel *data.AssetModel - } - tests := []struct { - name string - args args - want *AssetResolver - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := NewAssetResolver(tt.args.assetModel); !reflect.DeepEqual(got, tt.want) { - t.Errorf("NewAssetResolver() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAssetResolver_ResolveAssetReferences(t *testing.T) { - type fields struct { - assetModel *data.AssetModel - } - type args struct { - ctx context.Context - references []validators.AssetReference - } - tests := []struct { - name string - fields fields - args args - want []string - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ar := &AssetResolver{ - assetModel: tt.fields.assetModel, - } - got, err := ar.ResolveAssetReferences(tt.args.ctx, tt.args.references) - if (err != nil) != tt.wantErr { - t.Errorf("AssetResolver.ResolveAssetReferences() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("AssetResolver.ResolveAssetReferences() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAssetResolver_resolveAssetReference(t *testing.T) { - type fields struct { - assetModel *data.AssetModel - } - type args struct { - ctx context.Context - ref validators.AssetReference - } - tests := []struct { - name string - fields fields - args args - want string - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ar := &AssetResolver{ - assetModel: tt.fields.assetModel, - } - got, err := ar.resolveAssetReference(tt.args.ctx, tt.args.ref) - if (err != nil) != tt.wantErr { - t.Errorf("AssetResolver.resolveAssetReference() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("AssetResolver.resolveAssetReference() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAssetResolver_ValidateAssetIDs(t *testing.T) { - type fields struct { - assetModel *data.AssetModel - } - type args struct { - ctx context.Context - assetIDs []string - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ar := &AssetResolver{ - assetModel: tt.fields.assetModel, - } - if err := ar.ValidateAssetIDs(tt.args.ctx, tt.args.assetIDs); (err != nil) != tt.wantErr { - t.Errorf("AssetResolver.ValidateAssetIDs() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -}