From 96c323f2027a14c285a9cc6e6aae96d809a0b0c0 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:02:33 +0300 Subject: [PATCH 01/11] added the CreditSendPrefix and URL --- config/config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/config.go b/config/config.go index 5ac8438..a3917b3 100644 --- a/config/config.go +++ b/config/config.go @@ -26,6 +26,7 @@ const ( AliasResolverPrefix = "/api/v1/resolve" ExternalSMSPrefix = "/api/v1/external" AliasUpdatePrefix = "/api/v1/internal/update" + CreditSendPrefix = "/api/v1/credit-send" ) var ( @@ -57,6 +58,7 @@ var ( AliasResolverURL string ExternalSMSURL string AliasUpdateURL string + CreditSendURL string ) func setBase() error { @@ -105,6 +107,7 @@ func LoadConfig() error { AliasResolverURL, _ = url.JoinPath(aliasEnsURLBase, AliasResolverPrefix) ExternalSMSURL, _ = url.JoinPath(externalSMSBase, ExternalSMSPrefix) AliasUpdateURL, _ = url.JoinPath(aliasEnsURLBase, AliasUpdatePrefix) + CreditSendURL, _ = url.JoinPath(dataURLBase, CreditSendPrefix) return nil } -- 2.45.2 From 01569b9b3939f81188eb9804cfd1b114bf29382d Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:03:08 +0300 Subject: [PATCH 02/11] added the GetCreditSendMaxLimit function --- remote/account_service.go | 1 + remote/http/service.go | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/remote/account_service.go b/remote/account_service.go index c33fec3..b43b81a 100644 --- a/remote/account_service.go +++ b/remote/account_service.go @@ -30,4 +30,5 @@ type AccountService interface { PoolSwap(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapResult, error) GetSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) + GetCreditSendMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.CreditSendLimitsResult, error) } diff --git a/remote/http/service.go b/remote/http/service.go index f1f8d55..81e26f0 100644 --- a/remote/http/service.go +++ b/remote/http/service.go @@ -742,6 +742,26 @@ func (as *HTTPAccountService) SendPINResetSMS(ctx context.Context, admin, phone return nil } +// GetCreditSendMaxLimit calls the API to check credit limits and return the maxRAT and maxSAT +func (as *HTTPAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.CreditSendLimitsResult, error) { + var r models.CreditSendLimitsResult + + ep, err := url.JoinPath(config.CreditSendURL, poolAddress, fromTokenAddress, toTokenAddress, publicKey) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + // TODO: remove eth-custodial api dependency func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { var okResponse api.OKResponse -- 2.45.2 From c11060648d90bfbfd1d4370c316f67f765e5ce0e Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:04:12 +0300 Subject: [PATCH 03/11] added the CreditSendLimitsResult type for the API responses --- models/pool_swap_response.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/models/pool_swap_response.go b/models/pool_swap_response.go index 4f2c088..21d3d08 100644 --- a/models/pool_swap_response.go +++ b/models/pool_swap_response.go @@ -20,3 +20,8 @@ type MaxLimitResult struct { type TokenInPoolResult struct { CanSwapFrom bool `json:"canSwapFrom"` } + +type CreditSendLimitsResult struct { + MaxRAT string `json:"maxRAT"` + MaxSAT string `json:"maxSAT"` +} -- 2.45.2 From 3e82e16923bb8cfc05c466e3eab59a6f3ef4acf5 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:04:43 +0300 Subject: [PATCH 04/11] added the GetCreditSendMaxLimit to tests --- dev/api.go | 23 +++++++++++++++-------- testutil/mocks/service_mock.go | 5 +++++ testutil/testservice/account_service.go | 5 +++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/dev/api.go b/dev/api.go index 6000896..a18f4bb 100644 --- a/dev/api.go +++ b/dev/api.go @@ -587,10 +587,10 @@ func (das *DevAccountService) FetchVouchers(ctx context.Context, publicKey strin //TODO: Iterate over the account acc.Balances object for _, voucher := range das.vouchers { holdings = append(holdings, dataserviceapi.TokenHoldings{ - TokenAddress: voucher.Address, - TokenSymbol: voucher.Symbol, - TokenDecimals: strconv.Itoa(voucher.Decimals), - Balance: strconv.Itoa(int(defaultVoucherBalance)), + TokenAddress: voucher.Address, + TokenSymbol: voucher.Symbol, + TokenDecimals: strconv.Itoa(voucher.Decimals), + Balance: strconv.Itoa(int(defaultVoucherBalance)), }) } @@ -861,10 +861,10 @@ func (das *DevAccountService) GetPoolSwappableFromVouchers(ctx context.Context, } for _, v := range p.Vouchers { swapFromList = append(swapFromList, dataserviceapi.TokenHoldings{ - TokenAddress: v.Address, - TokenSymbol: v.Symbol, - TokenDecimals: string(defaultDecimals), - Balance: fmt.Sprintf("%f", defaultVoucherBalance), + TokenAddress: v.Address, + TokenSymbol: v.Symbol, + TokenDecimals: string(defaultDecimals), + Balance: fmt.Sprintf("%f", defaultVoucherBalance), }) } @@ -907,3 +907,10 @@ func (das *DevAccountService) CheckTokenInPool(ctx context.Context, poolAddress, CanSwapFrom: true, }, nil } + +func (das *DevAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.CreditSendLimitsResult, error) { + return &models.CreditSendLimitsResult{ + MaxRAT: "45599996", + MaxSAT: "3507692", + }, nil +} diff --git a/testutil/mocks/service_mock.go b/testutil/mocks/service_mock.go index 09ab50b..55e55c0 100644 --- a/testutil/mocks/service_mock.go +++ b/testutil/mocks/service_mock.go @@ -120,3 +120,8 @@ func (m MockAccountService) CheckTokenInPool(ctx context.Context, poolAddress, t args := m.Called(poolAddress, tokenAddress) return args.Get(0).(*models.TokenInPoolResult), args.Error(1) } + +func (m MockAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.CreditSendLimitsResult, error) { + args := m.Called(poolAddress, fromTokenAddress, toTokenAddress, publicKey) + return args.Get(0).(*models.CreditSendLimitsResult), args.Error(1) +} diff --git a/testutil/testservice/account_service.go b/testutil/testservice/account_service.go index 4294b6b..a0240f8 100644 --- a/testutil/testservice/account_service.go +++ b/testutil/testservice/account_service.go @@ -116,3 +116,8 @@ func (m TestAccountService) GetSwapFromTokenMaxLimit(ctx context.Context, poolAd func (m TestAccountService) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) { return &models.TokenInPoolResult{}, nil } + +func (m TestAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.CreditSendLimitsResult, error) { + return &models.CreditSendLimitsResult{}, nil +} + -- 2.45.2 From 75a7ec6b32eaaf025cad0d5e924fa62de0188057 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:06:01 +0300 Subject: [PATCH 05/11] correct the spelling of toTokenAddress --- remote/http/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/remote/http/service.go b/remote/http/service.go index 81e26f0..b69e00e 100644 --- a/remote/http/service.go +++ b/remote/http/service.go @@ -517,10 +517,10 @@ func (as *HTTPAccountService) GetSwapFromTokenMaxLimit(ctx context.Context, pool } } -func (as *HTTPAccountService) getSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokeAddress, publicKey string) (*models.MaxLimitResult, error) { +func (as *HTTPAccountService) getSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error) { var r models.MaxLimitResult - ep, err := url.JoinPath(config.PoolSwappableVouchersURL, poolAddress, "limit", fromTokenAddress, toTokeAddress, publicKey) + ep, err := url.JoinPath(config.PoolSwappableVouchersURL, poolAddress, "limit", fromTokenAddress, toTokenAddress, publicKey) if err != nil { return nil, err } -- 2.45.2 From af76541c86a91988e30d7b7008d79730a987013d Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:09:33 +0300 Subject: [PATCH 06/11] added the USD voucher to the TokenHoldings --- testutil/testservice/account_service.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/testutil/testservice/account_service.go b/testutil/testservice/account_service.go index a0240f8..4f5121d 100644 --- a/testutil/testservice/account_service.go +++ b/testutil/testservice/account_service.go @@ -34,12 +34,18 @@ func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { return []dataserviceapi.TokenHoldings{ - dataserviceapi.TokenHoldings{ + { TokenAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "2745987", }, + { + TokenAddress: "0x3f195a3F68BF4c6D49748eFa033a00C6634fF311", + TokenSymbol: "USD", + TokenDecimals: "6", + Balance: "4269100", + }, }, nil } @@ -120,4 +126,3 @@ func (m TestAccountService) CheckTokenInPool(ctx context.Context, poolAddress, t func (m TestAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.CreditSendLimitsResult, error) { return &models.CreditSendLimitsResult{}, nil } - -- 2.45.2 From a705443786fd41178db22994f076fdd854ca4c38 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:10:48 +0300 Subject: [PATCH 07/11] added a comment to describe the use of the TestAccountService --- testutil/testservice/account_service.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testutil/testservice/account_service.go b/testutil/testservice/account_service.go index 4f5121d..fcb7d87 100644 --- a/testutil/testservice/account_service.go +++ b/testutil/testservice/account_service.go @@ -8,6 +8,8 @@ import ( dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" ) +// This is used in the menu traversal tests + type TestAccountService struct { } -- 2.45.2 From 81ff6c40348f96696fe978caa5c3bfa65ee5bfaa Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:32:03 +0300 Subject: [PATCH 08/11] added the CreditSendReverseQuote prefix and URL --- config/config.go | 83 +++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/config/config.go b/config/config.go index a3917b3..e7ffa97 100644 --- a/config/config.go +++ b/config/config.go @@ -7,26 +7,27 @@ import ( ) const ( - createAccountPath = "/api/v2/account/create" - trackStatusPath = "/api/track" - balancePathPrefix = "/api/account" - trackPath = "/api/v2/account/status" - tokenTransferPrefix = "/api/v2/token/transfer" - voucherHoldingsPathPrefix = "/api/v1/holdings" - voucherTransfersPathPrefix = "/api/v1/transfers/last10" - voucherDataPathPrefix = "/api/v1/token" - SendSMSPrefix = "api/v1/external/upsell" - poolDepositPrefix = "/api/v2/pool/deposit" - poolSwapQoutePrefix = "/api/v2/pool/quote" - poolSwapPrefix = "/api/v2/pool/swap" - topPoolsPrefix = "/api/v1/pool/top" - retrievePoolDetailsPrefix = "/api/v1/pool/reverse" - poolSwappableVouchersPrefix = "/api/v1/pool" - AliasRegistrationPrefix = "/api/v1/internal/register" - AliasResolverPrefix = "/api/v1/resolve" - ExternalSMSPrefix = "/api/v1/external" - AliasUpdatePrefix = "/api/v1/internal/update" - CreditSendPrefix = "/api/v1/credit-send" + createAccountPath = "/api/v2/account/create" + trackStatusPath = "/api/track" + balancePathPrefix = "/api/account" + trackPath = "/api/v2/account/status" + tokenTransferPrefix = "/api/v2/token/transfer" + voucherHoldingsPathPrefix = "/api/v1/holdings" + voucherTransfersPathPrefix = "/api/v1/transfers/last10" + voucherDataPathPrefix = "/api/v1/token" + SendSMSPrefix = "api/v1/external/upsell" + poolDepositPrefix = "/api/v2/pool/deposit" + poolSwapQoutePrefix = "/api/v2/pool/quote" + poolSwapPrefix = "/api/v2/pool/swap" + topPoolsPrefix = "/api/v1/pool/top" + retrievePoolDetailsPrefix = "/api/v1/pool/reverse" + poolSwappableVouchersPrefix = "/api/v1/pool" + AliasRegistrationPrefix = "/api/v1/internal/register" + AliasResolverPrefix = "/api/v1/resolve" + ExternalSMSPrefix = "/api/v1/external" + AliasUpdatePrefix = "/api/v1/internal/update" + CreditSendPrefix = "/api/v1/credit-send" + CreditSendReverseQuotePrefix = "/api/v1/pool/reverse-quote" ) var ( @@ -39,26 +40,27 @@ var ( ) var ( - CreateAccountURL string - TrackStatusURL string - BalanceURL string - TrackURL string - TokenTransferURL string - VoucherHoldingsURL string - VoucherTransfersURL string - VoucherDataURL string - PoolDepositURL string - PoolSwapQuoteURL string - PoolSwapURL string - TopPoolsURL string - RetrievePoolDetailsURL string - PoolSwappableVouchersURL string - SendSMSURL string - AliasRegistrationURL string - AliasResolverURL string - ExternalSMSURL string - AliasUpdateURL string - CreditSendURL string + CreateAccountURL string + TrackStatusURL string + BalanceURL string + TrackURL string + TokenTransferURL string + VoucherHoldingsURL string + VoucherTransfersURL string + VoucherDataURL string + PoolDepositURL string + PoolSwapQuoteURL string + PoolSwapURL string + TopPoolsURL string + RetrievePoolDetailsURL string + PoolSwappableVouchersURL string + SendSMSURL string + AliasRegistrationURL string + AliasResolverURL string + ExternalSMSURL string + AliasUpdateURL string + CreditSendURL string + CreditSendReverseQuoteURL string ) func setBase() error { @@ -108,6 +110,7 @@ func LoadConfig() error { ExternalSMSURL, _ = url.JoinPath(externalSMSBase, ExternalSMSPrefix) AliasUpdateURL, _ = url.JoinPath(aliasEnsURLBase, AliasUpdatePrefix) CreditSendURL, _ = url.JoinPath(dataURLBase, CreditSendPrefix) + CreditSendReverseQuoteURL, _ = url.JoinPath(dataURLBase, CreditSendReverseQuotePrefix) return nil } -- 2.45.2 From 72af514cf383db63ad13c6e33582292c4928a83e Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:32:51 +0300 Subject: [PATCH 09/11] added the CreditSendReverseQouteResult type for API responses --- models/pool_swap_response.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/models/pool_swap_response.go b/models/pool_swap_response.go index 21d3d08..fae2255 100644 --- a/models/pool_swap_response.go +++ b/models/pool_swap_response.go @@ -25,3 +25,8 @@ type CreditSendLimitsResult struct { MaxRAT string `json:"maxRAT"` MaxSAT string `json:"maxSAT"` } + +type CreditSendReverseQouteResult struct { + InputAmount string `json:"inputAmount"` + OutputAmount string `json:"outputAmount"` +} -- 2.45.2 From 7eaa771eb43961104cfae8c0425d68a836adfa31 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:33:17 +0300 Subject: [PATCH 10/11] added the GetCreditSendReverseQuote function --- remote/account_service.go | 1 + remote/http/service.go | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/remote/account_service.go b/remote/account_service.go index b43b81a..29286ba 100644 --- a/remote/account_service.go +++ b/remote/account_service.go @@ -31,4 +31,5 @@ type AccountService interface { GetSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) GetCreditSendMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.CreditSendLimitsResult, error) + GetCreditSendReverseQuote(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, toTokenAMount string) (*models.CreditSendReverseQouteResult, error) } diff --git a/remote/http/service.go b/remote/http/service.go index b69e00e..a3c3e82 100644 --- a/remote/http/service.go +++ b/remote/http/service.go @@ -762,6 +762,26 @@ func (as *HTTPAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAdd return &r, nil } +// GetCreditSendReverseQuote calls the API to getthe reverse quote for sending RAT amount +func (as *HTTPAccountService) GetCreditSendReverseQuote(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, toTokenAMount string) (*models.CreditSendReverseQouteResult, error) { + var r models.CreditSendReverseQouteResult + + ep, err := url.JoinPath(config.CreditSendReverseQuoteURL, poolAddress, fromTokenAddress, toTokenAddress, toTokenAMount) + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + // TODO: remove eth-custodial api dependency func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { var okResponse api.OKResponse -- 2.45.2 From fe897cca84f23124f1a2ce67bde12be69219894e Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Tue, 28 Oct 2025 11:34:21 +0300 Subject: [PATCH 11/11] added the GetCreditSendReverseQuote to tests --- dev/api.go | 7 +++++++ testutil/mocks/service_mock.go | 5 +++++ testutil/testservice/account_service.go | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/dev/api.go b/dev/api.go index a18f4bb..97609b5 100644 --- a/dev/api.go +++ b/dev/api.go @@ -914,3 +914,10 @@ func (das *DevAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAdd MaxSAT: "3507692", }, nil } + +func (das *DevAccountService) GetCreditSendReverseQuote(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, toTokenAMount string) (*models.CreditSendReverseQouteResult, error) { + return &models.CreditSendReverseQouteResult{ + InputAmount: "3076923", + OutputAmount: "40000000", + }, nil +} diff --git a/testutil/mocks/service_mock.go b/testutil/mocks/service_mock.go index 55e55c0..6962bfb 100644 --- a/testutil/mocks/service_mock.go +++ b/testutil/mocks/service_mock.go @@ -125,3 +125,8 @@ func (m MockAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAddre args := m.Called(poolAddress, fromTokenAddress, toTokenAddress, publicKey) return args.Get(0).(*models.CreditSendLimitsResult), args.Error(1) } + +func (m MockAccountService) GetCreditSendReverseQuote(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, toTokenAMount string) (*models.CreditSendReverseQouteResult, error) { + args := m.Called(poolAddress, fromTokenAddress, toTokenAddress, toTokenAMount) + return args.Get(0).(*models.CreditSendReverseQouteResult), args.Error(1) +} diff --git a/testutil/testservice/account_service.go b/testutil/testservice/account_service.go index fcb7d87..7c0f320 100644 --- a/testutil/testservice/account_service.go +++ b/testutil/testservice/account_service.go @@ -128,3 +128,7 @@ func (m TestAccountService) CheckTokenInPool(ctx context.Context, poolAddress, t func (m TestAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.CreditSendLimitsResult, error) { return &models.CreditSendLimitsResult{}, nil } + +func (m TestAccountService) GetCreditSendReverseQuote(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, toTokenAMount string) (*models.CreditSendReverseQouteResult, error) { + return &models.CreditSendReverseQouteResult{}, nil +} -- 2.45.2