added the grassrootseconomics/go-vise package to sarafu-api #16

Merged
Alfred-mk merged 2 commits from update-go-vise into master 2025-11-19 11:22:32 +01:00
7 changed files with 200 additions and 44 deletions
Showing only changes of commit 74e5fb016c - Show all commits

View File

@ -7,25 +7,27 @@ import (
) )
const ( const (
createAccountPath = "/api/v2/account/create" createAccountPath = "/api/v2/account/create"
trackStatusPath = "/api/track" trackStatusPath = "/api/track"
balancePathPrefix = "/api/account" balancePathPrefix = "/api/account"
trackPath = "/api/v2/account/status" trackPath = "/api/v2/account/status"
tokenTransferPrefix = "/api/v2/token/transfer" tokenTransferPrefix = "/api/v2/token/transfer"
voucherHoldingsPathPrefix = "/api/v1/holdings" voucherHoldingsPathPrefix = "/api/v1/holdings"
voucherTransfersPathPrefix = "/api/v1/transfers/last10" voucherTransfersPathPrefix = "/api/v1/transfers/last10"
voucherDataPathPrefix = "/api/v1/token" voucherDataPathPrefix = "/api/v1/token"
SendSMSPrefix = "api/v1/external/upsell" SendSMSPrefix = "api/v1/external/upsell"
poolDepositPrefix = "/api/v2/pool/deposit" poolDepositPrefix = "/api/v2/pool/deposit"
poolSwapQoutePrefix = "/api/v2/pool/quote" poolSwapQoutePrefix = "/api/v2/pool/quote"
poolSwapPrefix = "/api/v2/pool/swap" poolSwapPrefix = "/api/v2/pool/swap"
topPoolsPrefix = "/api/v1/pool/top" topPoolsPrefix = "/api/v1/pool/top"
retrievePoolDetailsPrefix = "/api/v1/pool/reverse" retrievePoolDetailsPrefix = "/api/v1/pool/reverse"
poolSwappableVouchersPrefix = "/api/v1/pool" poolSwappableVouchersPrefix = "/api/v1/pool"
AliasRegistrationPrefix = "/api/v1/internal/register" AliasRegistrationPrefix = "/api/v1/internal/register"
AliasResolverPrefix = "/api/v1/resolve" AliasResolverPrefix = "/api/v1/resolve"
ExternalSMSPrefix = "/api/v1/external" ExternalSMSPrefix = "/api/v1/external"
AliasUpdatePrefix = "/api/v1/internal/update" AliasUpdatePrefix = "/api/v1/internal/update"
CreditSendPrefix = "/api/v1/credit-send"
CreditSendReverseQuotePrefix = "/api/v1/pool/reverse-quote"
) )
var ( var (
@ -38,25 +40,27 @@ var (
) )
var ( var (
CreateAccountURL string CreateAccountURL string
TrackStatusURL string TrackStatusURL string
BalanceURL string BalanceURL string
TrackURL string TrackURL string
TokenTransferURL string TokenTransferURL string
VoucherHoldingsURL string VoucherHoldingsURL string
VoucherTransfersURL string VoucherTransfersURL string
VoucherDataURL string VoucherDataURL string
PoolDepositURL string PoolDepositURL string
PoolSwapQuoteURL string PoolSwapQuoteURL string
PoolSwapURL string PoolSwapURL string
TopPoolsURL string TopPoolsURL string
RetrievePoolDetailsURL string RetrievePoolDetailsURL string
PoolSwappableVouchersURL string PoolSwappableVouchersURL string
SendSMSURL string SendSMSURL string
AliasRegistrationURL string AliasRegistrationURL string
AliasResolverURL string AliasResolverURL string
ExternalSMSURL string ExternalSMSURL string
AliasUpdateURL string AliasUpdateURL string
CreditSendURL string
CreditSendReverseQuoteURL string
) )
func setBase() error { func setBase() error {
@ -105,6 +109,8 @@ func LoadConfig() error {
AliasResolverURL, _ = url.JoinPath(aliasEnsURLBase, AliasResolverPrefix) AliasResolverURL, _ = url.JoinPath(aliasEnsURLBase, AliasResolverPrefix)
ExternalSMSURL, _ = url.JoinPath(externalSMSBase, ExternalSMSPrefix) ExternalSMSURL, _ = url.JoinPath(externalSMSBase, ExternalSMSPrefix)
AliasUpdateURL, _ = url.JoinPath(aliasEnsURLBase, AliasUpdatePrefix) AliasUpdateURL, _ = url.JoinPath(aliasEnsURLBase, AliasUpdatePrefix)
CreditSendURL, _ = url.JoinPath(dataURLBase, CreditSendPrefix)
CreditSendReverseQuoteURL, _ = url.JoinPath(dataURLBase, CreditSendReverseQuotePrefix)
return nil return nil
} }

View File

@ -907,3 +907,17 @@ func (das *DevAccountService) CheckTokenInPool(ctx context.Context, poolAddress,
CanSwapFrom: true, CanSwapFrom: true,
}, nil }, nil
} }
func (das *DevAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.CreditSendLimitsResult, error) {
return &models.CreditSendLimitsResult{
MaxRAT: "45599996",
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
}

View File

@ -20,3 +20,13 @@ type MaxLimitResult struct {
type TokenInPoolResult struct { type TokenInPoolResult struct {
CanSwapFrom bool `json:"canSwapFrom"` CanSwapFrom bool `json:"canSwapFrom"`
} }
type CreditSendLimitsResult struct {
MaxRAT string `json:"maxRAT"`
MaxSAT string `json:"maxSAT"`
}
type CreditSendReverseQouteResult struct {
InputAmount string `json:"inputAmount"`
OutputAmount string `json:"outputAmount"`
}

View File

@ -30,4 +30,6 @@ type AccountService interface {
PoolSwap(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapResult, error) 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) GetSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error)
CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, 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)
} }

View File

@ -27,11 +27,36 @@ var (
logg = slogging.Get().With("component", "sarafu-api.devapi") logg = slogging.Get().With("component", "sarafu-api.devapi")
) )
type APIError struct {
Code string
Description string
}
func (e *APIError) Error() string {
if e.Code != "" {
return fmt.Sprintf("[%s] %s", e.Code, e.Description)
}
return e.Description
}
type HTTPAccountService struct { type HTTPAccountService struct {
SS storage.StorageService SS storage.StorageService
UseApi bool UseApi bool
} }
// symbolReplacements holds mappings of invalid symbols → valid ones
var symbolReplacements = map[string]string{
"USD₮": "USDT",
}
// sanitizeSymbol replaces known invalid token symbols with normalized ones
func sanitizeSymbol(symbol string) string {
if replacement, ok := symbolReplacements[symbol]; ok {
return replacement
}
return symbol
}
// Parameters: // Parameters:
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to // - trackingId: A unique identifier for the account.This should be obtained from a previous call to
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the // CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
@ -130,6 +155,11 @@ func (as *HTTPAccountService) FetchVouchers(ctx context.Context, publicKey strin
return nil, err return nil, err
} }
// Normalize symbols before returning
for i := range r.Holdings {
r.Holdings[i].TokenSymbol = sanitizeSymbol(r.Holdings[i].TokenSymbol)
}
return r.Holdings, nil return r.Holdings, nil
} }
@ -156,6 +186,11 @@ func (as *HTTPAccountService) FetchTransactions(ctx context.Context, publicKey s
return nil, err return nil, err
} }
// Normalize symbols before returning
for i := range r.Transfers {
r.Transfers[i].TokenSymbol = sanitizeSymbol(r.Transfers[i].TokenSymbol)
}
return r.Transfers, nil return r.Transfers, nil
} }
@ -177,6 +212,9 @@ func (as *HTTPAccountService) VoucherData(ctx context.Context, address string) (
return nil, err return nil, err
} }
// Normalize symbols before returning
r.TokenDetails.TokenSymbol = sanitizeSymbol(r.TokenDetails.TokenSymbol)
_, err = doRequest(ctx, req, &r) _, err = doRequest(ctx, req, &r)
return &r.TokenDetails, err return &r.TokenDetails, err
} }
@ -367,7 +405,6 @@ func (as *HTTPAccountService) GetPoolSwappableFromVouchers(ctx context.Context,
svc := dev.NewDevAccountService(ctx, as.SS) svc := dev.NewDevAccountService(ctx, as.SS)
return svc.GetPoolSwappableFromVouchers(ctx, poolAddress, publicKey) return svc.GetPoolSwappableFromVouchers(ctx, poolAddress, publicKey)
} }
} }
func (as *HTTPAccountService) getPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) { func (as *HTTPAccountService) getPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
@ -383,6 +420,14 @@ func (as *HTTPAccountService) getPoolSwappableFromVouchers(ctx context.Context,
return nil, err return nil, err
} }
_, err = doRequest(ctx, req, &r) _, err = doRequest(ctx, req, &r)
if err != nil {
return nil, err
}
// Normalize symbols before returning
for i := range r.PoolSwappableVouchers {
r.PoolSwappableVouchers[i].TokenSymbol = sanitizeSymbol(r.PoolSwappableVouchers[i].TokenSymbol)
}
return r.PoolSwappableVouchers, nil return r.PoolSwappableVouchers, nil
} }
@ -423,6 +468,15 @@ func (as HTTPAccountService) getPoolSwappableVouchers(ctx context.Context, poolA
} }
_, err = doRequest(ctx, req, &r) _, err = doRequest(ctx, req, &r)
if err != nil {
return nil, err
}
// Normalize symbols before returning
for i := range r.PoolSwappableVouchers {
r.PoolSwappableVouchers[i].TokenSymbol = sanitizeSymbol(r.PoolSwappableVouchers[i].TokenSymbol)
}
return r.PoolSwappableVouchers, nil return r.PoolSwappableVouchers, nil
} }
@ -463,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 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 { if err != nil {
return nil, err return nil, err
} }
@ -688,6 +742,46 @@ func (as *HTTPAccountService) SendPINResetSMS(ctx context.Context, admin, phone
return nil 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
}
// 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 // TODO: remove eth-custodial api dependency
func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
var okResponse api.OKResponse var okResponse api.OKResponse
@ -720,7 +814,11 @@ func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKRespons
if err := json.Unmarshal(body, &errResponse); err != nil { if err := json.Unmarshal(body, &errResponse); err != nil {
return nil, err return nil, err
} }
return nil, errors.New(errResponse.Description)
return nil, &APIError{
Code: errResponse.ErrCode,
Description: errResponse.Description,
}
} }
if err := json.Unmarshal(body, &okResponse); err != nil { if err := json.Unmarshal(body, &okResponse); err != nil {
@ -728,7 +826,7 @@ func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKRespons
} }
if len(okResponse.Result) == 0 { if len(okResponse.Result) == 0 {
return nil, errors.New("Empty api result") return nil, errors.New("empty api result")
} }
v, err := json.Marshal(okResponse.Result) v, err := json.Marshal(okResponse.Result)

View File

@ -120,3 +120,13 @@ func (m MockAccountService) CheckTokenInPool(ctx context.Context, poolAddress, t
args := m.Called(poolAddress, tokenAddress) args := m.Called(poolAddress, tokenAddress)
return args.Get(0).(*models.TokenInPoolResult), args.Error(1) 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)
}
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)
}

View File

@ -8,6 +8,8 @@ import (
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
// This is used in the menu traversal tests
type TestAccountService struct { type TestAccountService struct {
} }
@ -34,12 +36,18 @@ func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
return []dataserviceapi.TokenHoldings{ return []dataserviceapi.TokenHoldings{
dataserviceapi.TokenHoldings{ {
TokenAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", TokenAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF", TokenSymbol: "SRF",
TokenDecimals: "6", TokenDecimals: "6",
Balance: "2745987", Balance: "2745987",
}, },
{
TokenAddress: "0x3f195a3F68BF4c6D49748eFa033a00C6634fF311",
TokenSymbol: "USD",
TokenDecimals: "6",
Balance: "4269100",
},
}, nil }, nil
} }
@ -116,3 +124,11 @@ func (m TestAccountService) GetSwapFromTokenMaxLimit(ctx context.Context, poolAd
func (m TestAccountService) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) { func (m TestAccountService) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) {
return &models.TokenInPoolResult{}, nil return &models.TokenInPoolResult{}, nil
} }
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
}