diff --git a/.github/workflows/anchor_platform_integration_check.yml b/.github/workflows/anchor_platform_integration_check.yml index 4de40c209..53f27271e 100644 --- a/.github/workflows/anchor_platform_integration_check.yml +++ b/.github/workflows/anchor_platform_integration_check.yml @@ -28,7 +28,7 @@ jobs: - name: Run Docker Compose for SDP and Anchor Platform working-directory: dev - run: docker-compose -f docker-compose-sdp-anchor.yml down && docker-compose -f docker-compose-sdp-anchor.yml up --build -d + run: docker compose -f docker-compose-sdp-anchor.yml down && docker compose -f docker-compose-sdp-anchor.yml up --build -d - name: Install curl run: sudo apt-get update && sudo apt-get install -y curl @@ -55,4 +55,4 @@ jobs: - name: Docker logs if: always() working-directory: dev - run: docker-compose -f docker-compose-sdp-anchor.yml logs && docker-compose -f docker-compose-sdp-anchor.yml down + run: docker compose -f docker-compose-sdp-anchor.yml logs && docker compose -f docker-compose-sdp-anchor.yml down diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 898a498f9..1378e6044 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,8 +30,6 @@ jobs: with: version: v1.56.2 # this is the golangci-lint version args: --timeout 5m0s - skip-build-cache: true - skip-pkg-cache: true - name: Run ./gomod.sh run: ./gomod.sh diff --git a/.github/workflows/e2e_integration_test.yml b/.github/workflows/e2e_integration_test.yml index 585a02ba0..4a5ad80e9 100644 --- a/.github/workflows/e2e_integration_test.yml +++ b/.github/workflows/e2e_integration_test.yml @@ -26,6 +26,7 @@ jobs: e2e: runs-on: ubuntu-latest strategy: + max-parallel: 1 matrix: platform: - "Stellar" @@ -46,12 +47,12 @@ jobs: - name: Cleanup data working-directory: internal/integrationtests/docker - run: docker-compose -f docker-compose-e2e-tests.yml down -v + run: docker compose -f docker-compose-e2e-tests.yml down -v shell: bash - name: Run Docker Compose for SDP, Anchor Platform and TSS working-directory: internal/integrationtests/docker - run: docker-compose -f docker-compose-e2e-tests.yml up --build -V -d + run: docker compose -f docker-compose-e2e-tests.yml up --build -V -d shell: bash - name: Install curl @@ -98,5 +99,5 @@ jobs: - name: Docker logs if: always() working-directory: internal/integrationtests/docker - run: docker-compose -f docker-compose-e2e-tests.yml logs && docker-compose -f docker-compose-e2e-tests.yml down + run: docker compose -f docker-compose-e2e-tests.yml logs && docker compose -f docker-compose-e2e-tests.yml down shell: bash diff --git a/.github/workflows/singletenant_to_multitenant_db_migration_test.yml b/.github/workflows/singletenant_to_multitenant_db_migration_test.yml index 3c0d78de3..6b9b0d2e2 100644 --- a/.github/workflows/singletenant_to_multitenant_db_migration_test.yml +++ b/.github/workflows/singletenant_to_multitenant_db_migration_test.yml @@ -31,12 +31,12 @@ jobs: - name: Cleanup data working-directory: internal/integrationtests/docker - run: docker-compose -f docker-compose-e2e-tests.yml down -v + run: docker compose -f docker-compose-e2e-tests.yml down -v shell: bash - name: Run Docker Compose for SDP, Anchor Platform and TSS working-directory: internal/integrationtests/docker - run: docker-compose -f docker-compose-e2e-tests.yml up --build -V -d + run: docker compose -f docker-compose-e2e-tests.yml up --build -V -d shell: bash - name: Install curl @@ -140,6 +140,6 @@ jobs: if: always() working-directory: internal/integrationtests/docker run: | - docker-compose -f docker-compose-e2e-tests.yml logs - docker-compose -f docker-compose-e2e-tests.yml down -v + docker compose -f docker-compose-e2e-tests.yml logs + docker compose -f docker-compose-e2e-tests.yml down -v shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index aea432dc9..09e3dcf9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). None +## [2.1.1](https://github.com/stellar/stellar-disbursement-platform-backend/releases/tag/2.1.1) ([diff](https://github.com/stellar/stellar-disbursement-platform-backend/compare/2.1.0...2.1.1)) + +### Changed +- Removed calls related to the deprecated Circle Accounts API and replaced them with calls to `GET /v1/businessAccount/balances` and `GET /configuration`. [#433](https://github.com/stellar/stellar-disbursement-platform-backend/pull/433). + ## [2.1.0](https://github.com/stellar/stellar-disbursement-platform-backend/releases/tag/2.1.0) ([diff](https://github.com/stellar/stellar-disbursement-platform-backend/compare/2.0.0...2.1.0)) Release of the Stellar Disbursement Platform v2.1.0. This release introduces diff --git a/dev/docker-compose-tss.yml b/dev/docker-compose-tss.yml index 3c101024e..fd562bad4 100644 --- a/dev/docker-compose-tss.yml +++ b/dev/docker-compose-tss.yml @@ -13,7 +13,7 @@ services: NETWORK_PASSPHRASE: "Test SDF Network ; September 2015" HORIZON_URL: "https://horizon-testnet.stellar.org" NUM_CHANNEL_ACCOUNTS: "3" - MAX_BASE_FEE: "100" + MAX_BASE_FEE: "1000000" TSS_METRICS_PORT: "9002" TSS_METRICS_TYPE: "TSS_PROMETHEUS" DISTRIBUTION_PUBLIC_KEY: ${DISTRIBUTION_PUBLIC_KEY} diff --git a/dev/main.sh b/dev/main.sh index 15d6699ce..ac2dd5781 100755 --- a/dev/main.sh +++ b/dev/main.sh @@ -34,11 +34,11 @@ fi # prepare echo $DIVIDER -echo "====> ๐Ÿ‘€ start calling docker-compose -p sdp-multi-tenant down" +echo "====> ๐Ÿ‘€ start calling docker compose -p sdp-multi-tenant down" docker ps -aq | xargs docker stop | xargs docker rm -#docker-compose -p sdp-multi-tenant down -docker-compose down -echo "====> โœ… finish calling docker-compose down" +#docker compose -p sdp-multi-tenant down +docker compose down +echo "====> โœ… finish calling docker compose down" # Run docker compose echo $DIVIDER @@ -67,11 +67,11 @@ fi echo $DIVIDER echo "====> ๐Ÿ‘€calling docker compose up" export GIT_COMMIT="debug" -docker-compose -p sdp-multi-tenant up -d --build +docker compose -p sdp-multi-tenant up -d --build # Run docker compose echo $DIVIDER -echo "====> โœ…finish calling docker-compose up" +echo "====> โœ…finish calling docker compose up" # Initialize tenants @@ -144,7 +144,7 @@ echo "====> โœ…Step 3: finished initialization of tenants" echo $DIVIDER # Initialize test_users echo "====> Step 4: initialize test users..." -docker-compose -p sdp-multi-tenant exec sdp-api ./dev/scripts/add_test_users.sh +docker compose -p sdp-multi-tenant exec sdp-api ./dev/scripts/add_test_users.sh echo $DIVIDER echo "๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ SUCCESS! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰" diff --git a/helmchart/sdp/Chart.yaml b/helmchart/sdp/Chart.yaml index 463527fd2..5b945f8bb 100644 --- a/helmchart/sdp/Chart.yaml +++ b/helmchart/sdp/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 name: stellar-disbursement-platform description: A Helm chart for the Stellar Disbursement Platform Backend (A.K.A. `sdp`) -version: "2.1.0" -appVersion: "2.1.0" +version: "2.1.1" +appVersion: "2.1.1" type: application maintainers: - name: Stellar Development Foundation diff --git a/internal/anchorplatform/sep24_auth_middleware.go b/internal/anchorplatform/sep24_auth_middleware.go index 166e0ad8b..ccf3af66e 100644 --- a/internal/anchorplatform/sep24_auth_middleware.go +++ b/internal/anchorplatform/sep24_auth_middleware.go @@ -2,6 +2,7 @@ package anchorplatform import ( "context" + "errors" "fmt" "net/http" "strings" @@ -40,14 +41,14 @@ func checkSEP24ClientAndHomeDomains(ctx context.Context, sep24Claims *SEP24JWTCl missingDomain := "missing client domain in the token claims" if networkPassphrase == network.PublicNetworkPassphrase { log.Ctx(ctx).Error(missingDomain) - return fmt.Errorf(missingDomain) + return errors.New(missingDomain) } log.Ctx(ctx).Warn(missingDomain) } if sep24Claims.HomeDomain() == "" { missingDomain := "missing home domain in the token claims" log.Ctx(ctx).Error(missingDomain) - return fmt.Errorf(missingDomain) + return errors.New(missingDomain) } return nil } diff --git a/internal/circle/account_configuration.go b/internal/circle/account_configuration.go new file mode 100644 index 000000000..ed0ed7fd9 --- /dev/null +++ b/internal/circle/account_configuration.go @@ -0,0 +1,32 @@ +package circle + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// ConfigurationResponse represents the response containing account configuration. +type ConfigurationResponse struct { + Data AccountConfiguration `json:"data,omitempty"` +} + +// AccountConfiguration represents the configuration settings of an account. +type AccountConfiguration struct { + Payments WalletConfig `json:"payments,omitempty"` +} + +// WalletConfig represents the wallet configuration with details such as the master wallet ID. +type WalletConfig struct { + MasterWalletID string `json:"masterWalletId,omitempty"` +} + +// parseAccountConfigurationResponse parses the response containing account configuration. +func parseAccountConfigurationResponse(resp *http.Response) (*AccountConfiguration, error) { + var configurationResponse ConfigurationResponse + if err := json.NewDecoder(resp.Body).Decode(&configurationResponse); err != nil { + return nil, fmt.Errorf("decoding response: %w", err) + } + + return &configurationResponse.Data, nil +} diff --git a/internal/circle/balance.go b/internal/circle/balance.go index dd7062313..7966f9e25 100644 --- a/internal/circle/balance.go +++ b/internal/circle/balance.go @@ -1,7 +1,10 @@ package circle import ( + "encoding/json" "errors" + "fmt" + "net/http" "github.com/stellar/stellar-disbursement-platform-backend/internal/data" "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" @@ -13,6 +16,17 @@ var ( ErrUnsupportedCurrencyForNetwork = errors.New("unsupported Circle currency code for this network type") ) +// ListBusinessBalancesResponse represents the response containing business balances. +type ListBusinessBalancesResponse struct { + Data Balances `json:"data,omitempty"` +} + +// Balances represents the available and unsettled balances for different currencies. +type Balances struct { + Available []Balance `json:"available"` + Unsettled []Balance `json:"unsettled"` +} + // Balance represents the amount and currency of a balance or transfer. type Balance struct { Amount string `json:"amount"` @@ -34,12 +48,12 @@ var AllowedAssetsMap = map[string]map[utils.NetworkType]data.Asset{ // ParseStellarAsset returns the Stellar asset for the given Circle currency code, or an error if the currency is not // supported in the SDP. func ParseStellarAsset(circleCurrency string, networkType utils.NetworkType) (data.Asset, error) { - return ParseStellarAssetFromAllowlist(circleCurrency, networkType, AllowedAssetsMap) + return parseStellarAssetFromAllowlist(circleCurrency, networkType, AllowedAssetsMap) } -// ParseStellarAssetFromAllowlist returns the Stellar asset for the given Circle currency code, or an error if the +// parseStellarAssetFromAllowlist returns the Stellar asset for the given Circle currency code, or an error if the // currency is not supported in the SDP. This function allows for the use of a custom asset allowlist. -func ParseStellarAssetFromAllowlist(circleCurrency string, networkType utils.NetworkType, allowedAssetsMap map[string]map[utils.NetworkType]data.Asset) (data.Asset, error) { +func parseStellarAssetFromAllowlist(circleCurrency string, networkType utils.NetworkType, allowedAssetsMap map[string]map[utils.NetworkType]data.Asset) (data.Asset, error) { assetByNetworkType, ok := allowedAssetsMap[circleCurrency] if !ok { return data.Asset{}, ErrUnsupportedCurrency @@ -52,3 +66,13 @@ func ParseStellarAssetFromAllowlist(circleCurrency string, networkType utils.Net return asset, nil } + +// parseBusinessBalancesResponse parses the response from the Circle API into a Balances struct. +func parseBusinessBalancesResponse(resp *http.Response) (*Balances, error) { + var balancesResponse ListBusinessBalancesResponse + if err := json.NewDecoder(resp.Body).Decode(&balancesResponse); err != nil { + return nil, fmt.Errorf("unmarshalling Circle HTTP response: %w", err) + } + + return &balancesResponse.Data, nil +} diff --git a/internal/circle/balance_test.go b/internal/circle/balance_test.go index 279b3ac39..b0a79d366 100644 --- a/internal/circle/balance_test.go +++ b/internal/circle/balance_test.go @@ -87,7 +87,7 @@ func Test_ParseStellarAsset(t *testing.T) { }) t.Run("FromAllowlist/"+tc.name, func(t *testing.T) { - asset, err := ParseStellarAssetFromAllowlist(tc.circleCurrency, tc.networkType, tc.allowedAssetsMap) + asset, err := parseStellarAssetFromAllowlist(tc.circleCurrency, tc.networkType, tc.allowedAssetsMap) if tc.expectedError == nil { assert.NoError(t, err) diff --git a/internal/circle/client.go b/internal/circle/client.go index 0eace1e58..768b0e114 100644 --- a/internal/circle/client.go +++ b/internal/circle/client.go @@ -21,9 +21,10 @@ import ( ) const ( - pingPath = "/ping" - transferPath = "/v1/transfers" - walletPath = "/v1/wallets" + pingPath = "/ping" + transferPath = "/v1/transfers" + businessBalancesPath = "/v1/businessAccount/balances" + configurationPath = "/v1/configuration" ) var authErrorStatusCodes = []int{http.StatusUnauthorized, http.StatusForbidden} @@ -35,7 +36,8 @@ type ClientInterface interface { Ping(ctx context.Context) (bool, error) PostTransfer(ctx context.Context, transferRequest TransferRequest) (*Transfer, error) GetTransferByID(ctx context.Context, id string) (*Transfer, error) - GetWalletByID(ctx context.Context, id string) (*Wallet, error) + GetBusinessBalances(ctx context.Context) (*Balances, error) + GetAccountConfiguration(ctx context.Context) (*AccountConfiguration, error) } // Client provides methods to interact with the Circle API. @@ -157,11 +159,32 @@ func (client *Client) GetTransferByID(ctx context.Context, id string) (*Transfer return parseTransferResponse(resp) } -// GetWalletByID retrieves a wallet by its ID. -// -// Circle API documentation: https://developers.circle.com/circle-mint/reference/getwallet. -func (client *Client) GetWalletByID(ctx context.Context, id string) (*Wallet, error) { - url, err := url.JoinPath(client.BasePath, walletPath, id) +// GetBusinessBalances retrieves the available and unsettled balances for different currencies. +func (client *Client) GetBusinessBalances(ctx context.Context) (*Balances, error) { + url, err := url.JoinPath(client.BasePath, businessBalancesPath) + if err != nil { + return nil, fmt.Errorf("building path: %w", err) + } + + resp, err := client.request(ctx, url, http.MethodGet, true, nil) + if err != nil { + return nil, fmt.Errorf("making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + handleErr := client.handleError(ctx, resp) + if handleErr != nil { + return nil, fmt.Errorf("handling API response error: %w", handleErr) + } + } + + return parseBusinessBalancesResponse(resp) +} + +// GetAccountConfiguration retrieves the configuration of the Circle Account. +func (client *Client) GetAccountConfiguration(ctx context.Context) (*AccountConfiguration, error) { + url, err := url.JoinPath(client.BasePath, configurationPath) if err != nil { return nil, fmt.Errorf("building path: %w", err) } @@ -179,7 +202,7 @@ func (client *Client) GetWalletByID(ctx context.Context, id string) (*Wallet, er } } - return parseWalletResponse(resp) + return parseAccountConfigurationResponse(resp) } type RetryableError struct { diff --git a/internal/circle/client_mock.go b/internal/circle/client_mock.go index 418dcb81b..0cadf1ee2 100644 --- a/internal/circle/client_mock.go +++ b/internal/circle/client_mock.go @@ -13,29 +13,29 @@ type MockClient struct { mock.Mock } -// GetTransferByID provides a mock function with given fields: ctx, id -func (_m *MockClient) GetTransferByID(ctx context.Context, id string) (*Transfer, error) { - ret := _m.Called(ctx, id) +// GetAccountConfiguration provides a mock function with given fields: ctx +func (_m *MockClient) GetAccountConfiguration(ctx context.Context) (*AccountConfiguration, error) { + ret := _m.Called(ctx) if len(ret) == 0 { - panic("no return value specified for GetTransferByID") + panic("no return value specified for GetAccountConfiguration") } - var r0 *Transfer + var r0 *AccountConfiguration var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*Transfer, error)); ok { - return rf(ctx, id) + if rf, ok := ret.Get(0).(func(context.Context) (*AccountConfiguration, error)); ok { + return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context, string) *Transfer); ok { - r0 = rf(ctx, id) + if rf, ok := ret.Get(0).(func(context.Context) *AccountConfiguration); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*Transfer) + r0 = ret.Get(0).(*AccountConfiguration) } } - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, id) + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) } else { r1 = ret.Error(1) } @@ -43,24 +43,54 @@ func (_m *MockClient) GetTransferByID(ctx context.Context, id string) (*Transfer return r0, r1 } -// GetWalletByID provides a mock function with given fields: ctx, id -func (_m *MockClient) GetWalletByID(ctx context.Context, id string) (*Wallet, error) { +// GetBusinessBalances provides a mock function with given fields: ctx +func (_m *MockClient) GetBusinessBalances(ctx context.Context) (*Balances, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetBusinessBalances") + } + + var r0 *Balances + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*Balances, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *Balances); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*Balances) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransferByID provides a mock function with given fields: ctx, id +func (_m *MockClient) GetTransferByID(ctx context.Context, id string) (*Transfer, error) { ret := _m.Called(ctx, id) if len(ret) == 0 { - panic("no return value specified for GetWalletByID") + panic("no return value specified for GetTransferByID") } - var r0 *Wallet + var r0 *Transfer var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*Wallet, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) (*Transfer, error)); ok { return rf(ctx, id) } - if rf, ok := ret.Get(0).(func(context.Context, string) *Wallet); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) *Transfer); ok { r0 = rf(ctx, id) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*Wallet) + r0 = ret.Get(0).(*Transfer) } } diff --git a/internal/circle/client_test.go b/internal/circle/client_test.go index f6959fa34..31f193b53 100644 --- a/internal/circle/client_test.go +++ b/internal/circle/client_test.go @@ -215,9 +215,9 @@ func Test_Client_GetTransferByID(t *testing.T) { }) } -func Test_Client_GetWalletByID(t *testing.T) { +func Test_Client_GetBusinessBalances(t *testing.T) { ctx := context.Background() - t.Run("get wallet by id error", func(t *testing.T) { + t.Run("get business balances error", func(t *testing.T) { cc, httpClientMock, _ := newClientWithMocks(t) testError := errors.New("test error") httpClientMock. @@ -226,19 +226,19 @@ func Test_Client_GetWalletByID(t *testing.T) { req, ok := args.Get(0).(*http.Request) assert.True(t, ok) - assert.Equal(t, "http://localhost:8080/v1/wallets/test-id", req.URL.String()) + assert.Equal(t, "http://localhost:8080/v1/businessAccount/balances", req.URL.String()) assert.Equal(t, http.MethodGet, req.Method) assert.Equal(t, "Bearer test-key", req.Header.Get("Authorization")) }). Return(nil, testError). Once() - wallet, err := cc.GetWalletByID(ctx, "test-id") - assert.EqualError(t, err, fmt.Errorf("making request: submitting request to http://localhost:8080/v1/wallets/test-id: %w", testError).Error()) + wallet, err := cc.GetBusinessBalances(ctx) + assert.EqualError(t, err, fmt.Errorf("making request: submitting request to http://localhost:8080/v1/businessAccount/balances: %w", testError).Error()) assert.Nil(t, wallet) }) - t.Run("get wallet by id fails auth", func(t *testing.T) { + t.Run("get business balances fails auth", func(t *testing.T) { const unauthorizedResponse = `{ "code": 401, "message": "Malformed key. Does it contain three parts?" @@ -258,24 +258,22 @@ func Test_Client_GetWalletByID(t *testing.T) { On("DeactivateTenantDistributionAccount", mock.Anything, tnt.ID). Return(nil).Once() - transfer, err := cc.GetWalletByID(ctx, "test-id") + transfer, err := cc.GetBusinessBalances(ctx) assert.EqualError(t, err, "handling API response error: Circle API error: APIError: Code=401, Message=Malformed key. Does it contain three parts?, Errors=[], StatusCode=401") assert.Nil(t, transfer) }) - t.Run("get wallet by id successful", func(t *testing.T) { + t.Run("get business balances successful", func(t *testing.T) { const getWalletResponseJSON = `{ "data": { - "walletId": "test-id", - "entityId": "2f47c999-9022-4939-acea-dc3afa9ccbaf", - "type": "end_user_wallet", - "description": "Treasury Wallet", - "balances": [ - { - "amount": "4790.00", - "currency": "USD" - } - ] + "available": [ + { + "amount": "22306.90", + "currency": "USD" + } + ], + "unsettled": [] + } } }` cc, httpClientMock, _ := newClientWithMocks(t) @@ -289,24 +287,105 @@ func Test_Client_GetWalletByID(t *testing.T) { req, ok := args.Get(0).(*http.Request) assert.True(t, ok) - assert.Equal(t, "http://localhost:8080/v1/wallets/test-id", req.URL.String()) + assert.Equal(t, "http://localhost:8080/v1/businessAccount/balances", req.URL.String()) assert.Equal(t, http.MethodGet, req.Method) assert.Equal(t, "Bearer test-key", req.Header.Get("Authorization")) }). Once() - wallet, err := cc.GetWalletByID(ctx, "test-id") + businessBalances, err := cc.GetBusinessBalances(ctx) assert.NoError(t, err) - wantWallet := &Wallet{ - WalletID: "test-id", - EntityID: "2f47c999-9022-4939-acea-dc3afa9ccbaf", - Type: "end_user_wallet", - Description: "Treasury Wallet", - Balances: []Balance{ - {Amount: "4790.00", Currency: "USD"}, + wantBusinessBalances := &Balances{ + Available: []Balance{ + {Amount: "22306.90", Currency: "USD"}, }, + Unsettled: []Balance{}, } - assert.Equal(t, wantWallet, wallet) + assert.Equal(t, wantBusinessBalances, businessBalances) + }) +} + +func Test_Client_GetAccountConfiguration(t *testing.T) { + ctx := context.Background() + t.Run("get configuration error", func(t *testing.T) { + cc, httpClientMock, _ := newClientWithMocks(t) + testError := errors.New("test error") + httpClientMock. + On("Do", mock.Anything). + Run(func(args mock.Arguments) { + req, ok := args.Get(0).(*http.Request) + assert.True(t, ok) + + assert.Equal(t, "http://localhost:8080/v1/configuration", req.URL.String()) + assert.Equal(t, http.MethodGet, req.Method) + assert.Equal(t, "Bearer test-key", req.Header.Get("Authorization")) + }). + Return(nil, testError). + Once() + + wallet, err := cc.GetAccountConfiguration(ctx) + assert.EqualError(t, err, fmt.Errorf("making request: submitting request to http://localhost:8080/v1/configuration: %w", testError).Error()) + assert.Nil(t, wallet) + }) + + t.Run("get configuration fails auth", func(t *testing.T) { + const unauthorizedResponse = `{ + "code": 401, + "message": "Malformed key. Does it contain three parts?" + }` + cc, httpClientMock, tntManagerMock := newClientWithMocks(t) + tnt := &tenant.Tenant{ID: "test-id"} + ctx = tenant.SaveTenantInContext(ctx, tnt) + + httpClientMock. + On("Do", mock.Anything). + Return(&http.Response{ + StatusCode: http.StatusUnauthorized, + Body: io.NopCloser(bytes.NewBufferString(unauthorizedResponse)), + }, nil). + Once() + tntManagerMock. + On("DeactivateTenantDistributionAccount", mock.Anything, tnt.ID). + Return(nil).Once() + + transfer, err := cc.GetAccountConfiguration(ctx) + assert.EqualError(t, err, "handling API response error: Circle API error: APIError: Code=401, Message=Malformed key. Does it contain three parts?, Errors=[], StatusCode=401") + assert.Nil(t, transfer) + }) + + t.Run("get configuration successful", func(t *testing.T) { + const getConfigurationResponseJSON = `{ + "data": { + "payments": { + "masterWalletId": "1016352538" + } + } + }` + cc, httpClientMock, _ := newClientWithMocks(t) + httpClientMock. + On("Do", mock.Anything). + Return(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(getConfigurationResponseJSON)), + }, nil). + Run(func(args mock.Arguments) { + req, ok := args.Get(0).(*http.Request) + assert.True(t, ok) + + assert.Equal(t, "http://localhost:8080/v1/configuration", req.URL.String()) + assert.Equal(t, http.MethodGet, req.Method) + assert.Equal(t, "Bearer test-key", req.Header.Get("Authorization")) + }). + Once() + + config, err := cc.GetAccountConfiguration(ctx) + assert.NoError(t, err) + wantConfig := &AccountConfiguration{ + Payments: WalletConfig{ + MasterWalletID: "1016352538", + }, + } + assert.Equal(t, wantConfig, config) }) } diff --git a/internal/circle/service.go b/internal/circle/service.go index 71ac39318..64d96eb64 100644 --- a/internal/circle/service.go +++ b/internal/circle/service.go @@ -138,10 +138,18 @@ func (s *Service) GetTransferByID(ctx context.Context, transferID string) (*Tran return client.GetTransferByID(ctx, transferID) } -func (s *Service) GetWalletByID(ctx context.Context, walletID string) (*Wallet, error) { +func (s *Service) GetBusinessBalances(ctx context.Context) (*Balances, error) { client, err := s.getClientForTenantInContext(ctx) if err != nil { return nil, fmt.Errorf("cannot get Circle client: %w", err) } - return client.GetWalletByID(ctx, walletID) + return client.GetBusinessBalances(ctx) +} + +func (s *Service) GetAccountConfiguration(ctx context.Context) (*AccountConfiguration, error) { + client, err := s.getClientForTenantInContext(ctx) + if err != nil { + return nil, fmt.Errorf("cannot get Circle client: %w", err) + } + return client.GetAccountConfiguration(ctx) } diff --git a/internal/circle/service_mock.go b/internal/circle/service_mock.go index e67346fdc..7291272bb 100644 --- a/internal/circle/service_mock.go +++ b/internal/circle/service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package circle @@ -13,29 +13,29 @@ type MockService struct { mock.Mock } -// GetTransferByID provides a mock function with given fields: ctx, id -func (_m *MockService) GetTransferByID(ctx context.Context, id string) (*Transfer, error) { - ret := _m.Called(ctx, id) +// GetAccountConfiguration provides a mock function with given fields: ctx +func (_m *MockService) GetAccountConfiguration(ctx context.Context) (*AccountConfiguration, error) { + ret := _m.Called(ctx) if len(ret) == 0 { - panic("no return value specified for GetTransferByID") + panic("no return value specified for GetAccountConfiguration") } - var r0 *Transfer + var r0 *AccountConfiguration var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*Transfer, error)); ok { - return rf(ctx, id) + if rf, ok := ret.Get(0).(func(context.Context) (*AccountConfiguration, error)); ok { + return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context, string) *Transfer); ok { - r0 = rf(ctx, id) + if rf, ok := ret.Get(0).(func(context.Context) *AccountConfiguration); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*Transfer) + r0 = ret.Get(0).(*AccountConfiguration) } } - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, id) + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) } else { r1 = ret.Error(1) } @@ -43,24 +43,54 @@ func (_m *MockService) GetTransferByID(ctx context.Context, id string) (*Transfe return r0, r1 } -// GetWalletByID provides a mock function with given fields: ctx, id -func (_m *MockService) GetWalletByID(ctx context.Context, id string) (*Wallet, error) { +// GetBusinessBalances provides a mock function with given fields: ctx +func (_m *MockService) GetBusinessBalances(ctx context.Context) (*Balances, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetBusinessBalances") + } + + var r0 *Balances + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*Balances, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *Balances); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*Balances) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransferByID provides a mock function with given fields: ctx, id +func (_m *MockService) GetTransferByID(ctx context.Context, id string) (*Transfer, error) { ret := _m.Called(ctx, id) if len(ret) == 0 { - panic("no return value specified for GetWalletByID") + panic("no return value specified for GetTransferByID") } - var r0 *Wallet + var r0 *Transfer var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*Wallet, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) (*Transfer, error)); ok { return rf(ctx, id) } - if rf, ok := ret.Get(0).(func(context.Context, string) *Wallet); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) *Transfer); ok { r0 = rf(ctx, id) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*Wallet) + r0 = ret.Get(0).(*Transfer) } } diff --git a/internal/circle/service_test.go b/internal/circle/service_test.go index 02a27160a..1f24f27e9 100644 --- a/internal/circle/service_test.go +++ b/internal/circle/service_test.go @@ -259,16 +259,47 @@ func Test_Service_allMethods(t *testing.T) { assert.Equal(t, &Transfer{ID: "transfer-id"}, res) }) - t.Run("GetWalletByID", func(t *testing.T) { + t.Run("GetAccountConfiguration", func(t *testing.T) { + mClient := NewMockClient(t) + mClient. + On("GetAccountConfiguration", ctx). + Return(&AccountConfiguration{ + Payments: WalletConfig{ + MasterWalletID: "master-wallet-id", + }, + }, nil). + Once() + svc := createService(t, mClient) + + res, err := svc.GetAccountConfiguration(ctx) + assert.NoError(t, err) + assert.Equal(t, &AccountConfiguration{ + Payments: WalletConfig{ + MasterWalletID: "master-wallet-id", + }, + }, res) + }) + + t.Run("GetBusinessBalances", func(t *testing.T) { mCircleClient := NewMockClient(t) mCircleClient. - On("GetWalletByID", ctx, "wallet-id"). - Return(&Wallet{WalletID: "wallet-id"}, nil). + On("GetBusinessBalances", ctx). + Return(&Balances{ + Available: []Balance{ + {Currency: "USD", Amount: "1234"}, + }, + Unsettled: []Balance{}, + }, nil). Once() svc := createService(t, mCircleClient) - res, err := svc.GetWalletByID(ctx, "wallet-id") + res, err := svc.GetBusinessBalances(ctx) assert.NoError(t, err) - assert.Equal(t, &Wallet{WalletID: "wallet-id"}, res) + assert.Equal(t, &Balances{ + Available: []Balance{ + {Currency: "USD", Amount: "1234"}, + }, + Unsettled: []Balance{}, + }, res) }) } diff --git a/internal/circle/wallet.go b/internal/circle/wallet.go deleted file mode 100644 index 7b2284d0f..000000000 --- a/internal/circle/wallet.go +++ /dev/null @@ -1,29 +0,0 @@ -package circle - -import ( - "encoding/json" - "fmt" - "net/http" -) - -type WalletResponse struct { - Data Wallet `json:"data"` -} - -type Wallet struct { - WalletID string `json:"walletId"` - EntityID string `json:"entityId"` - Type string `json:"type"` - Description string `json:"description"` - Balances []Balance `json:"balances"` -} - -// parseWalletResponse parses the response from the Circle API into a Wallet struct. -func parseWalletResponse(resp *http.Response) (*Wallet, error) { - var walletResponse WalletResponse - if err := json.NewDecoder(resp.Body).Decode(&walletResponse); err != nil { - return nil, fmt.Errorf("unmarshalling Circle HTTP response: %w", err) - } - - return &walletResponse.Data, nil -} diff --git a/internal/data/disbursement_instructions.go b/internal/data/disbursement_instructions.go index c7493c979..2281a2df4 100644 --- a/internal/data/disbursement_instructions.go +++ b/internal/data/disbursement_instructions.go @@ -65,7 +65,7 @@ var ( // | | |--- Check if the receiver wallet exists. // | | | |--- If the receiver wallet does not exist, create one. // | | | |--- If the receiver wallet exists and it's not REGISTERED, retry the invitation SMS. -// | | |--- Delete all payments tied to this disbursement. +// | | |--- Delete all previously existing payments tied to this disbursement. // | | |--- Create all payments passed in the instructions. func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, userID string, instructions []*DisbursementInstruction, disbursement *Disbursement, update *DisbursementUpdate, maxNumberOfInstructions int) error { if len(instructions) > maxNumberOfInstructions { @@ -85,6 +85,7 @@ func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, userID st return fmt.Errorf("error fetching receivers by phone number: %w", err) } + // Create a map of existing receivers for easy lookup receiverMap := make(map[string]*Receiver) for _, receiver := range existingReceivers { receiverMap[receiver.PhoneNumber] = receiver @@ -98,6 +99,7 @@ func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, userID st } } + // Create missing receivers for _, instruction := range instructions { _, exists := receiverMap[instruction.Phone] if !exists { diff --git a/internal/integrationtests/docker/docker-compose-e2e-tests.yml b/internal/integrationtests/docker/docker-compose-e2e-tests.yml index b4547721b..11f5e81c8 100644 --- a/internal/integrationtests/docker/docker-compose-e2e-tests.yml +++ b/internal/integrationtests/docker/docker-compose-e2e-tests.yml @@ -124,7 +124,7 @@ services: NETWORK_PASSPHRASE: "Test SDF Network ; September 2015" HORIZON_URL: "https://horizon-testnet.stellar.org" NUM_CHANNEL_ACCOUNTS: "1" - MAX_BASE_FEE: "100000" + MAX_BASE_FEE: "1000000" TSS_METRICS_PORT: "9002" TSS_METRICS_TYPE: "TSS_PROMETHEUS" DISTRIBUTION_PUBLIC_KEY: ${DISTRIBUTION_PUBLIC_KEY} diff --git a/internal/integrationtests/scripts/e2e_integration_test.sh b/internal/integrationtests/scripts/e2e_integration_test.sh index 185ad74bb..444d7decf 100755 --- a/internal/integrationtests/scripts/e2e_integration_test.sh +++ b/internal/integrationtests/scripts/e2e_integration_test.sh @@ -41,7 +41,7 @@ for accountType in "${accountTypes[@]}"; do # Run docker compose echo $DIVIDER echo "====> ๐Ÿ‘€Step 2: build sdp-api, anchor-platform and tss" - docker-compose -f ../docker/docker-compose-e2e-tests.yml up --build -d + docker compose -f ../docker/docker-compose-e2e-tests.yml up --build -d wait_for_server "http://localhost:8000/health" 20 echo "====> โœ…Step 2: finishing build" diff --git a/internal/integrationtests/scripts/singletenant_to_multitenant_db_migration_test.sh b/internal/integrationtests/scripts/singletenant_to_multitenant_db_migration_test.sh index 41e4e98c4..46b11c334 100755 --- a/internal/integrationtests/scripts/singletenant_to_multitenant_db_migration_test.sh +++ b/internal/integrationtests/scripts/singletenant_to_multitenant_db_migration_test.sh @@ -33,7 +33,7 @@ echo "====> โœ…Step 1: finish preparation" # Run docker compose echo $DIVIDER echo "====> ๐Ÿ‘€Step 2: build sdp-api, anchor-platform and tss" -docker-compose -f ../docker/docker-compose-e2e-tests.yml up --build -d +docker compose -f ../docker/docker-compose-e2e-tests.yml up --build -d wait_for_server "http://localhost:8000/health" 20 echo "====> โœ…Step 2: finishing build" diff --git a/internal/serve/httphandler/balances_handler.go b/internal/serve/httphandler/balances_handler.go index 64bb958d0..5c4256ece 100644 --- a/internal/serve/httphandler/balances_handler.go +++ b/internal/serve/httphandler/balances_handler.go @@ -55,13 +55,13 @@ func (h BalancesHandler) Get(w http.ResponseWriter, r *http.Request) { return } - circleWallet, err := h.CircleService.GetWalletByID(ctx, distAccount.CircleWalletID) + businessBalances, err := h.CircleService.GetBusinessBalances(ctx) if err != nil { wrapCircleError(ctx, err).Render(w) return } - balances := h.filterBalances(ctx, circleWallet) + balances := h.filterBalances(ctx, businessBalances.Available) response := GetBalanceResponse{ Account: distAccount, @@ -70,9 +70,9 @@ func (h BalancesHandler) Get(w http.ResponseWriter, r *http.Request) { httpjson.Render(w, response, httpjson.JSON) } -func (h BalancesHandler) filterBalances(ctx context.Context, circleWallet *circle.Wallet) []Balance { +func (h BalancesHandler) filterBalances(ctx context.Context, availableBalances []circle.Balance) []Balance { balances := []Balance{} - for _, balance := range circleWallet.Balances { + for _, balance := range availableBalances { asset, err := circle.ParseStellarAsset(balance.Currency, h.NetworkType) if err != nil { log.Ctx(ctx).Debugf("Ignoring balance for asset %s, as it's not supported by the SDP: %v", balance.Currency, err) diff --git a/internal/serve/httphandler/balances_handler_test.go b/internal/serve/httphandler/balances_handler_test.go index 0212d5f5c..be2c322ba 100644 --- a/internal/serve/httphandler/balances_handler_test.go +++ b/internal/serve/httphandler/balances_handler_test.go @@ -79,7 +79,7 @@ func Test_BalancesHandler_Get(t *testing.T) { Once() mCircleService. - On("GetWalletByID", mock.Anything, "circle-wallet-id"). + On("GetBusinessBalances", mock.Anything). Return(nil, fmt.Errorf("wrapped error: %w", circleAPIError)). Once() }, @@ -125,7 +125,7 @@ func Test_BalancesHandler_Get(t *testing.T) { }, nil). Once() mCircleService. - On("GetWalletByID", mock.Anything, "circle-wallet-id"). + On("GetBusinessBalances", mock.Anything). Return(nil, errors.New("unexpected error")). Once() }, @@ -145,15 +145,12 @@ func Test_BalancesHandler_Get(t *testing.T) { }, nil). Once() mCircleService. - On("GetWalletByID", mock.Anything, "circle-wallet-id"). - Return(&circle.Wallet{ - WalletID: "test-id", - EntityID: "2f47c999-9022-4939-acea-dc3afa9ccbaf", - Type: "end_user_wallet", - Description: "Treasury Wallet", - Balances: []circle.Balance{ + On("GetBusinessBalances", mock.Anything). + Return(&circle.Balances{ + Available: []circle.Balance{ {Amount: "123.00", Currency: "USD"}, }, + Unsettled: []circle.Balance{}, }, nil). Once() }, @@ -183,16 +180,14 @@ func Test_BalancesHandler_Get(t *testing.T) { Status: schema.AccountStatusActive, }, nil). Once() + mCircleService. - On("GetWalletByID", mock.Anything, "circle-wallet-id"). - Return(&circle.Wallet{ - WalletID: "test-id", - EntityID: "2f47c999-9022-4939-acea-dc3afa9ccbaf", - Type: "end_user_wallet", - Description: "Treasury Wallet", - Balances: []circle.Balance{ + On("GetBusinessBalances", mock.Anything). + Return(&circle.Balances{ + Available: []circle.Balance{ {Amount: "123.00", Currency: "USD"}, }, + Unsettled: []circle.Balance{}, }, nil). Once() }, @@ -245,14 +240,14 @@ func Test_BalancesHandler_filterBalances(t *testing.T) { testCases := []struct { name string networkType utils.NetworkType - circleWallet *circle.Wallet + circleBalances *circle.Balances expectedBalances []Balance }{ { name: "[Pubnet] only supported assets are included", networkType: utils.PubnetNetworkType, - circleWallet: &circle.Wallet{ - Balances: []circle.Balance{ + circleBalances: &circle.Balances{ + Available: []circle.Balance{ {Currency: "FOO", Amount: "100"}, {Currency: "USD", Amount: "200"}, {Currency: "BAR", Amount: "300"}, @@ -275,8 +270,8 @@ func Test_BalancesHandler_filterBalances(t *testing.T) { { name: "[Testnet] only supported assets are included", networkType: utils.TestnetNetworkType, - circleWallet: &circle.Wallet{ - Balances: []circle.Balance{ + circleBalances: &circle.Balances{ + Available: []circle.Balance{ {Currency: "FOO", Amount: "100"}, {Currency: "USD", Amount: "200"}, {Currency: "BAR", Amount: "300"}, @@ -299,8 +294,8 @@ func Test_BalancesHandler_filterBalances(t *testing.T) { { name: "[Pubnet] none of the provided assets is supported", networkType: utils.PubnetNetworkType, - circleWallet: &circle.Wallet{ - Balances: []circle.Balance{ + circleBalances: &circle.Balances{ + Available: []circle.Balance{ {Currency: "FOO", Amount: "100"}, }, }, @@ -309,8 +304,8 @@ func Test_BalancesHandler_filterBalances(t *testing.T) { { name: "[Testnet] none of the provided assets is supported", networkType: utils.TestnetNetworkType, - circleWallet: &circle.Wallet{ - Balances: []circle.Balance{ + circleBalances: &circle.Balances{ + Available: []circle.Balance{ {Currency: "FOO", Amount: "100"}, }, }, @@ -322,7 +317,7 @@ func Test_BalancesHandler_filterBalances(t *testing.T) { t.Run(tc.name, func(t *testing.T) { h := BalancesHandler{NetworkType: tc.networkType} - actualBalances := h.filterBalances(ctx, tc.circleWallet) + actualBalances := h.filterBalances(ctx, tc.circleBalances.Available) assert.Equal(t, tc.expectedBalances, actualBalances) }) diff --git a/internal/serve/httphandler/circle_config_handler.go b/internal/serve/httphandler/circle_config_handler.go index 1aad847e6..c6524d2d6 100644 --- a/internal/serve/httphandler/circle_config_handler.go +++ b/internal/serve/httphandler/circle_config_handler.go @@ -176,10 +176,13 @@ func (h CircleConfigHandler) validateConfigWithCircle(ctx context.Context, patch // validate incoming WalletID if patchRequest.WalletID != nil { - _, err := circleClient.GetWalletByID(ctx, walletID) + accountConfig, err := circleClient.GetAccountConfiguration(ctx) if err != nil { return wrapCircleError(ctx, err) } + if accountConfig.Payments.MasterWalletID != walletID { + return httperror.BadRequest("The provided wallet ID does not match the master wallet ID from Circle", nil, nil) + } } return nil diff --git a/internal/serve/httphandler/circle_config_handler_test.go b/internal/serve/httphandler/circle_config_handler_test.go index 7555ddad6..9331d17d4 100644 --- a/internal/serve/httphandler/circle_config_handler_test.go +++ b/internal/serve/httphandler/circle_config_handler_test.go @@ -148,8 +148,8 @@ func TestCircleConfigHandler_Patch(t *testing.T) { Return(true, nil). Once() mCircleClient. - On("GetWalletByID", mock.Anything, "new_wallet_id"). - Return(&circle.Wallet{WalletID: "new_wallet_id"}, nil). + On("GetAccountConfiguration", mock.Anything). + Return(&circle.AccountConfiguration{Payments: circle.WalletConfig{MasterWalletID: "new_wallet_id"}}, nil). Once() mTenantManager. @@ -289,20 +289,33 @@ func Test_CircleConfigHandler_validateConfigWithCircle(t *testing.T) { expectedError: httperror.BadRequest("Failed to ping, please make sure that the provided API Key is correct.", nil, nil), }, { - name: "returns an error if circleClient.GetWalletByID returns an error", + name: "returns an error if circleClient.GetAccountConfiguration returns an error", patchRequest: PatchCircleConfigRequest{APIKey: &newAPIKey, WalletID: &newWalletID}, prepareMocksFn: func(t *testing.T, mEncrypter *utils.PrivateKeyEncrypterMock, mCircleClientConfigModel *circle.MockClientConfigModel, mCircleClient *circle.MockClient) { mCircleClient. On("Ping", ctx). Return(true, nil). Once() - mCircleClient. - On("GetWalletByID", ctx, newWalletID). + mCircleClient.On("GetAccountConfiguration", ctx). Return(nil, fmt.Errorf("get wallet error")). Once() }, expectedError: wrapCircleError(ctx, fmt.Errorf("get wallet error")), }, + { + name: "returns an error if wallet ID does not match the master wallet ID from Circle", + patchRequest: PatchCircleConfigRequest{APIKey: &newAPIKey, WalletID: &newWalletID}, + prepareMocksFn: func(t *testing.T, mEncrypter *utils.PrivateKeyEncrypterMock, mCircleClientConfigModel *circle.MockClientConfigModel, mCircleClient *circle.MockClient) { + mCircleClient. + On("Ping", ctx). + Return(true, nil). + Once() + mCircleClient.On("GetAccountConfiguration", ctx). + Return(&circle.AccountConfiguration{Payments: circle.WalletConfig{MasterWalletID: "another_wallet_id"}}, nil). + Once() + }, + expectedError: httperror.BadRequest("The provided wallet ID does not match the master wallet ID from Circle", nil, nil), + }, { name: "๐ŸŽ‰ successfully validate for a new pair of apiKey and walletID", patchRequest: PatchCircleConfigRequest{APIKey: &newAPIKey, WalletID: &newWalletID}, @@ -312,8 +325,8 @@ func Test_CircleConfigHandler_validateConfigWithCircle(t *testing.T) { Return(true, nil). Once() mCircleClient. - On("GetWalletByID", ctx, newWalletID). - Return(&circle.Wallet{WalletID: newWalletID}, nil). + On("GetAccountConfiguration", ctx). + Return(&circle.AccountConfiguration{Payments: circle.WalletConfig{MasterWalletID: newWalletID}}, nil). Once() }, expectedError: nil, @@ -346,8 +359,8 @@ func Test_CircleConfigHandler_validateConfigWithCircle(t *testing.T) { Return("api-key", nil). Once() mCircleClient. - On("GetWalletByID", ctx, newWalletID). - Return(&circle.Wallet{WalletID: newWalletID}, nil). + On("GetAccountConfiguration", ctx). + Return(&circle.AccountConfiguration{Payments: circle.WalletConfig{MasterWalletID: newWalletID}}, nil). Once() }, expectedError: nil, diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go index b6107305b..09bd35edd 100644 --- a/internal/serve/httphandler/disbursement_handler.go +++ b/internal/serve/httphandler/disbursement_handler.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "slices" "time" "github.com/go-chi/chi/v5" @@ -126,7 +127,7 @@ func (d DisbursementHandler) PostDisbursement(w http.ResponseWriter, r *http.Req newId, err := d.Models.Disbursements.Insert(ctx, &disbursement) if err != nil { - if errors.Is(data.ErrRecordAlreadyExists, err) { + if errors.Is(err, data.ErrRecordAlreadyExists) { httperror.Conflict("disbursement already exists", err, nil).Render(w) } else { httperror.BadRequest("could not create disbursement", err, nil).Render(w) @@ -203,7 +204,7 @@ func (d DisbursementHandler) PostDisbursementInstructions(w http.ResponseWriter, } // check if disbursement is in draft, ready status - if disbursement.Status != data.DraftDisbursementStatus && disbursement.Status != data.ReadyDisbursementStatus { + if !slices.Contains([]data.DisbursementStatus{data.DraftDisbursementStatus, data.ReadyDisbursementStatus}, disbursement.Status) { httperror.BadRequest("disbursement is not in draft or ready status", nil, nil).Render(w) return } diff --git a/internal/serve/httphandler/payments_handler.go b/internal/serve/httphandler/payments_handler.go index 039ee17e3..a63be3d4f 100644 --- a/internal/serve/httphandler/payments_handler.go +++ b/internal/serve/httphandler/payments_handler.go @@ -88,7 +88,7 @@ func (p PaymentsHandler) GetPayment(w http.ResponseWriter, r *http.Request) { payment, err := p.Models.Payment.Get(r.Context(), paymentID, p.DBConnectionPool) if err != nil { - if errors.Is(data.ErrRecordNotFound, err) { + if errors.Is(err, data.ErrRecordNotFound) { errorResponse := fmt.Sprintf("Cannot retrieve payment with ID: %s", paymentID) httperror.NotFound(errorResponse, err, nil).Render(w) return diff --git a/internal/serve/httphandler/statistics_handler.go b/internal/serve/httphandler/statistics_handler.go index 59038bced..1568f0a00 100644 --- a/internal/serve/httphandler/statistics_handler.go +++ b/internal/serve/httphandler/statistics_handler.go @@ -35,7 +35,7 @@ func (s StatisticsHandler) GetStatisticsByDisbursement(w http.ResponseWriter, r stats, err := statistics.CalculateStatisticsByDisbursement(ctx, s.DBConnectionPool, disbursementID) if err != nil { - if errors.Is(statistics.ErrResourcesNotFound, err) { + if errors.Is(err, statistics.ErrResourcesNotFound) { errorMsg := fmt.Sprintf("a disbursement with the id %s does not exist", disbursementID) httperror.NotFound(errorMsg, err, nil).Render(w) return diff --git a/internal/serve/serve.go b/internal/serve/serve.go index dff12a2b0..77296800c 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -469,7 +469,11 @@ func handleHTTP(o ServeOptions) *chi.Mux { }.ServeHTTP) // This loads the SEP-24 PII registration webpage. sep24HeaderTokenAuthenticationMiddleware := anchorplatform.SEP24HeaderTokenAuthenticateMiddleware(o.sep24JWTManager, o.NetworkPassphrase, o.tenantManager, o.SingleTenantMode) - r.With(sep24HeaderTokenAuthenticationMiddleware).Post("/otp", httphandler.ReceiverSendOTPHandler{Models: o.Models, SMSMessengerClient: o.SMSMessengerClient, ReCAPTCHAValidator: reCAPTCHAValidator}.ServeHTTP) + r.With(sep24HeaderTokenAuthenticationMiddleware).Post("/otp", httphandler.ReceiverSendOTPHandler{ + Models: o.Models, + SMSMessengerClient: o.SMSMessengerClient, + ReCAPTCHAValidator: reCAPTCHAValidator, + }.ServeHTTP) r.With(sep24HeaderTokenAuthenticationMiddleware).Post("/verification", httphandler.VerifyReceiverRegistrationHandler{ AnchorPlatformAPIService: o.AnchorPlatformAPIService, Models: o.Models, diff --git a/internal/services/disbursement_management_service_test.go b/internal/services/disbursement_management_service_test.go index ab0dfcee8..123e8aab2 100644 --- a/internal/services/disbursement_management_service_test.go +++ b/internal/services/disbursement_management_service_test.go @@ -247,10 +247,9 @@ func Test_DisbursementManagementService_StartDisbursement_success(t *testing.T) } prepareCircleServiceMockFn := func(mCircleService *circle.MockService) { mCircleService. - On("GetWalletByID", ctx, circleDistAccountDBVault.CircleWalletID). - Return(&circle.Wallet{ - WalletID: circleDistAccountDBVault.CircleWalletID, - Balances: []circle.Balance{ + On("GetBusinessBalances", mock.Anything). + Return(&circle.Balances{ + Available: []circle.Balance{ {Currency: "EUR", Amount: "10000000.0"}, }, }, nil). diff --git a/internal/services/distribution_account_service.go b/internal/services/distribution_account_service.go index b5eb42eaf..07747aa03 100644 --- a/internal/services/distribution_account_service.go +++ b/internal/services/distribution_account_service.go @@ -153,13 +153,13 @@ func (s *CircleDistributionAccountService) GetBalances(ctx context.Context, acco return nil, fmt.Errorf("This organization's distribution account is in %s state, please complete the %s activation process to access this endpoint.", account.Status, account.Type.Platform()) } - wallet, err := s.CircleService.GetWalletByID(ctx, account.CircleWalletID) + businessBalances, err := s.CircleService.GetBusinessBalances(ctx) if err != nil { return nil, fmt.Errorf("getting wallet by ID: %w", err) } balances := make(map[data.Asset]float64) - for _, b := range wallet.Balances { + for _, b := range businessBalances.Available { asset, err := circle.ParseStellarAsset(b.Currency, s.NetworkType) if err != nil { log.Ctx(ctx).Debugf("Ignoring balance for asset %s, as it's not supported by the SDP: %v", b.Currency, err) diff --git a/internal/services/distribution_account_service_test.go b/internal/services/distribution_account_service_test.go index be69f21cf..4b185809e 100644 --- a/internal/services/distribution_account_service_test.go +++ b/internal/services/distribution_account_service_test.go @@ -263,7 +263,7 @@ func Test_CircleDistributionAccountService_GetBalances(t *testing.T) { account: circleDistAcc, prepareMocksFn: func(mCircleService *circle.MockService) { mCircleService. - On("GetWalletByID", ctx, circleDistAcc.CircleWalletID). + On("GetBusinessBalances", ctx). Return(nil, errors.New("foobar")). Once() }, @@ -275,14 +275,14 @@ func Test_CircleDistributionAccountService_GetBalances(t *testing.T) { account: circleDistAcc, prepareMocksFn: func(mCircleService *circle.MockService) { mCircleService. - On("GetWalletByID", ctx, circleDistAcc.CircleWalletID). - Return(&circle.Wallet{ - WalletID: circleDistAcc.CircleWalletID, - Balances: []circle.Balance{ + On("GetBusinessBalances", ctx). + Return(&circle.Balances{ + Available: []circle.Balance{ {Currency: "USD", Amount: "100.0"}, {Currency: "EUR", Amount: "200.0"}, {Currency: "UNSUPPORTED_ASSET", Amount: "300.0"}, }, + Unsettled: []circle.Balance{}, }, nil). Once() }, @@ -297,10 +297,9 @@ func Test_CircleDistributionAccountService_GetBalances(t *testing.T) { account: circleDistAcc, prepareMocksFn: func(mCircleService *circle.MockService) { mCircleService. - On("GetWalletByID", ctx, circleDistAcc.CircleWalletID). - Return(&circle.Wallet{ - WalletID: circleDistAcc.CircleWalletID, - Balances: []circle.Balance{ + On("GetBusinessBalances", ctx). + Return(&circle.Balances{ + Available: []circle.Balance{ {Currency: "USD", Amount: "100.0"}, {Currency: "EUR", Amount: "200.0"}, {Currency: "UNSUPPORTED_ASSET", Amount: "300.0"}, @@ -346,12 +345,11 @@ func Test_CircleDistributionAccountService_GetBalance(t *testing.T) { Status: schema.AccountStatusActive, } unsupportedAsset := data.Asset{Code: "FOO", Issuer: "GCANIBF4EHC5ZKKMSPX2WFGJ4ZO7BI4JFHZHBUQC5FH3JOOLKG7F5DL3"} - mockGetWalletByIDFn := func(mCircleService *circle.MockService) { + mockGetGetBusinessBalancesFn := func(mCircleService *circle.MockService) { mCircleService. - On("GetWalletByID", ctx, circleDistAcc.CircleWalletID). - Return(&circle.Wallet{ - WalletID: circleDistAcc.CircleWalletID, - Balances: []circle.Balance{ + On("GetBusinessBalances", ctx). + Return(&circle.Balances{ + Available: []circle.Balance{ {Currency: "USD", Amount: "100.0"}, {Currency: "EUR", Amount: "200.0"}, }, @@ -380,7 +378,7 @@ func Test_CircleDistributionAccountService_GetBalance(t *testing.T) { networkType: utils.TestnetNetworkType, account: circleDistAcc, asset: unsupportedAsset, - prepareMocksFn: mockGetWalletByIDFn, + prepareMocksFn: mockGetGetBusinessBalancesFn, expectedError: fmt.Errorf("balance for asset %v not found for distribution account", unsupportedAsset), }, { @@ -388,7 +386,7 @@ func Test_CircleDistributionAccountService_GetBalance(t *testing.T) { networkType: utils.TestnetNetworkType, account: circleDistAcc, asset: assets.USDCAssetTestnet, - prepareMocksFn: mockGetWalletByIDFn, + prepareMocksFn: mockGetGetBusinessBalancesFn, expectedBalance: 100.0, }, { @@ -396,7 +394,7 @@ func Test_CircleDistributionAccountService_GetBalance(t *testing.T) { networkType: utils.PubnetNetworkType, account: circleDistAcc, asset: assets.EURCAssetPubnet, - prepareMocksFn: mockGetWalletByIDFn, + prepareMocksFn: mockGetGetBusinessBalancesFn, expectedBalance: 200.0, }, } diff --git a/internal/utils/float_test.go b/internal/utils/float_test.go index d200da138..d8abd855b 100644 --- a/internal/utils/float_test.go +++ b/internal/utils/float_test.go @@ -19,7 +19,7 @@ func Test_FloatToString(t *testing.T) { } for _, tc := range testCases { - t.Run(fmt.Sprintf(tc.wantStringOutput), func(t *testing.T) { + t.Run(fmt.Sprint(tc.wantStringOutput), func(t *testing.T) { gotStringOutput := FloatToString(tc.floatInput) assert.Equal(t, tc.wantStringOutput, gotStringOutput) }) diff --git a/main.go b/main.go index b31a7d136..ce517a4a9 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( // Version is the official version of this application. Whenever it's changed // here, it also needs to be updated at the `helmchart/Chart.yaml#appVersionโ€œ. -const Version = "2.1.0" +const Version = "2.1.1" // GitCommit is populated at build time by // go build -ldflags "-X main.GitCommit=$GIT_COMMIT" diff --git a/stellar-multitenant/internal/httphandler/tenants_handler.go b/stellar-multitenant/internal/httphandler/tenants_handler.go index 9ccea2544..f06d742e6 100644 --- a/stellar-multitenant/internal/httphandler/tenants_handler.go +++ b/stellar-multitenant/internal/httphandler/tenants_handler.go @@ -63,7 +63,7 @@ func (t TenantsHandler) GetByIDOrName(w http.ResponseWriter, r *http.Request) { tnt, err := t.Manager.GetTenantByIDOrName(ctx, arg) if err != nil { - if errors.Is(tenant.ErrTenantDoesNotExist, err) { + if errors.Is(err, tenant.ErrTenantDoesNotExist) { errorMsg := fmt.Sprintf("tenant %s does not exist", arg) httperror.NotFound(errorMsg, err, nil).Render(w) return @@ -210,12 +210,12 @@ func (t TenantsHandler) Patch(w http.ResponseWriter, r *http.Request) { Status: reqBody.Status, }) if err != nil { - if errors.Is(tenant.ErrEmptyUpdateTenant, err) { + if errors.Is(err, tenant.ErrEmptyUpdateTenant) { errorMsg := fmt.Sprintf("updating tenant %s: %s", tenantID, err) httperror.BadRequest(errorMsg, err, nil).Render(w) return } - if errors.Is(tenant.ErrTenantDoesNotExist, err) { + if errors.Is(err, tenant.ErrTenantDoesNotExist) { errorMsg := fmt.Sprintf("updating tenant: tenant %s does not exist", tenantID) httperror.NotFound(errorMsg, err, nil).Render(w) return @@ -236,7 +236,7 @@ func (t TenantsHandler) Delete(w http.ResponseWriter, r *http.Request) { Filters: map[tenant.FilterKey]interface{}{tenant.FilterKeyID: tenantID}, }) if err != nil { - if errors.Is(tenant.ErrTenantDoesNotExist, err) { + if errors.Is(err, tenant.ErrTenantDoesNotExist) { errorMsg := fmt.Sprintf("tenant %s does not exist", tenantID) httperror.NotFound(errorMsg, err, nil).Render(w) return diff --git a/v1_compatibility/database_migration_compatibility.sh b/v1_compatibility/database_migration_compatibility.sh index a81f9bdb8..632cf929a 100755 --- a/v1_compatibility/database_migration_compatibility.sh +++ b/v1_compatibility/database_migration_compatibility.sh @@ -17,8 +17,8 @@ echo "====> โœ…Step 1: finish cloning SDP v1 (stellar/stellar-relief-backoffice- # Run docker compose echo $DIVIDER echo "====> ๐Ÿ‘€Step 2: start calling docker compose up" -docker compose down && docker-compose up --abort-on-container-exit -echo "====> โœ…Step 2: finish calling docker-compose up" +docker compose down && docker compose up --abort-on-container-exit +echo "====> โœ…Step 2: finish calling docker compose up" echo $DIVIDER echo "๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ SUCCESS! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰" \ No newline at end of file