Compare commits

...

4 Commits

7 changed files with 91 additions and 10 deletions

View File

@ -28,15 +28,18 @@ const (
AliasUpdatePrefix = "/api/v1/internal/update"
CreditSendPrefix = "/api/v1/credit-send"
CreditSendReverseQuotePrefix = "/api/v1/pool/reverse-quote"
MpesaOnrampPath = "/api/v1/trigger-onramp"
)
var (
custodialURLBase string
dataURLBase string
BearerToken string
aliasEnsURLBase string
externalSMSBase string
IncludeStablesParam string
custodialURLBase string
dataURLBase string
BearerToken string
aliasEnsURLBase string
externalSMSBase string
IncludeStablesParam string
MpesaOnrampBearerToken string
mpesaOnrampBase string
)
var (
@ -61,6 +64,7 @@ var (
AliasUpdateURL string
CreditSendURL string
CreditSendReverseQuoteURL string
MpesaOnrampURL string
)
func setBase() error {
@ -72,6 +76,8 @@ func setBase() error {
externalSMSBase = env.GetEnv("EXTERNAL_SMS_BASE", "http://localhost:5035")
BearerToken = env.GetEnv("BEARER_TOKEN", "")
IncludeStablesParam = env.GetEnv("INCLUDE_STABLES_PARAM", "false")
MpesaOnrampBearerToken = env.GetEnv("MPESA_BEARER_TOKEN", "")
mpesaOnrampBase = env.GetEnv("MPESA_ONRAMP_BASE", "https://pretium.v1.grassecon.net")
_, err = url.Parse(custodialURLBase)
if err != nil {
@ -111,6 +117,7 @@ func LoadConfig() error {
AliasUpdateURL, _ = url.JoinPath(aliasEnsURLBase, AliasUpdatePrefix)
CreditSendURL, _ = url.JoinPath(dataURLBase, CreditSendPrefix)
CreditSendReverseQuoteURL, _ = url.JoinPath(dataURLBase, CreditSendReverseQuotePrefix)
MpesaOnrampURL, _ = url.JoinPath(mpesaOnrampBase, MpesaOnrampPath)
return nil
}

View File

@ -921,3 +921,11 @@ func (das *DevAccountService) GetCreditSendReverseQuote(ctx context.Context, poo
OutputAmount: "40000000",
}, nil
}
func (das *DevAccountService) MpesaTriggerOnramp(ctx context.Context, address, phoneNumber, asset string, amount int) (*models.MpesaOnrampResponse, error) {
return &models.MpesaOnrampResponse{
Message: "Success, kindly accept prompt sent.",
Status: "PENDING",
TransactionCode: "ae6fb33b-4653-4f38-a3b6-85dfea7a1e99",
}, nil
}

View File

@ -0,0 +1,7 @@
package models
type MpesaOnrampResponse struct {
Message string `json:"message"`
Status string `json:"status"`
TransactionCode string `json:"transactionCode"`
}

View File

@ -32,4 +32,5 @@ type AccountService interface {
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)
MpesaTriggerOnramp(ctx context.Context, address, phoneNumber, asset string, amount int) (*models.MpesaOnrampResponse, error)
}

View File

@ -10,7 +10,6 @@ import (
"log"
"net/http"
"net/url"
"regexp"
"strings"
"git.grassecon.net/grassrootseconomics/sarafu-api/config"
@ -23,8 +22,7 @@ import (
)
var (
aliasRegex = regexp.MustCompile("^\\+?[a-zA-Z0-9\\-_]+$")
logg = slogging.Get().With("component", "sarafu-api.devapi")
logg = slogging.Get().With("component", "sarafu-api.devapi")
)
type APIError struct {
@ -32,6 +30,10 @@ type APIError struct {
Description string
}
type ctxKey string
const ctxKeyAuthToken ctxKey = "authToken"
func (e *APIError) Error() string {
if e.Code != "" {
return fmt.Sprintf("[%s] %s", e.Code, e.Description)
@ -782,12 +784,59 @@ func (as *HTTPAccountService) GetCreditSendReverseQuote(ctx context.Context, poo
return &r, nil
}
// MpesaTriggerOnramp calls the API to perform an STK Push.
// Parameters:
// - address: The user's public key.
// - phoneNumber: The user's phone number
// - asset: the intented USD voucher "USDT | USDC | cUSD"
// - amount: The amount in Kenyan shillings
func (as *HTTPAccountService) MpesaTriggerOnramp(ctx context.Context, address, phoneNumber, asset string, amount int) (*models.MpesaOnrampResponse, error) {
var r models.MpesaOnrampResponse
ctx = context.WithValue(ctx, ctxKeyAuthToken, config.MpesaOnrampBearerToken)
// Prepare payload
payload := struct {
Address string `json:"address"`
PhoneNumber string `json:"phoneNumber"`
Asset string `json:"asset"`
Amount int `json:"amount"`
}{
Address: strings.TrimSpace(address),
PhoneNumber: strings.TrimSpace(phoneNumber),
Asset: strings.TrimSpace(asset),
Amount: amount,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal mpesa onramp payload: %w", err)
}
req, err := http.NewRequest("POST", config.MpesaOnrampURL, bytes.NewBuffer(payloadBytes))
if err != nil {
return nil, err
}
if _, err := doRequest(ctx, req, &r); 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
var errResponse api.ErrResponse
req.Header.Set("Authorization", "Bearer "+config.BearerToken)
// Check if a custom Authorization token was provided
if token, ok := ctx.Value(ctxKeyAuthToken).(string); ok && token != "" {
req.Header.Set("Authorization", "Bearer "+token)
} else {
req.Header.Set("Authorization", "Bearer "+config.BearerToken)
}
req.Header.Set("Content-Type", "application/json")
// Log request

View File

@ -130,3 +130,8 @@ func (m MockAccountService) GetCreditSendReverseQuote(ctx context.Context, poolA
args := m.Called(poolAddress, fromTokenAddress, toTokenAddress, toTokenAMount)
return args.Get(0).(*models.CreditSendReverseQouteResult), args.Error(1)
}
func (m MockAccountService) MpesaTriggerOnramp(ctx context.Context, address, phoneNumber, asset string, amount int) (*models.MpesaOnrampResponse, error) {
args := m.Called(address, phoneNumber, asset, amount)
return args.Get(0).(*models.MpesaOnrampResponse), args.Error(1)
}

View File

@ -132,3 +132,7 @@ func (m TestAccountService) GetCreditSendMaxLimit(ctx context.Context, poolAddre
func (m TestAccountService) GetCreditSendReverseQuote(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, toTokenAMount string) (*models.CreditSendReverseQouteResult, error) {
return &models.CreditSendReverseQouteResult{}, nil
}
func (m TestAccountService) MpesaTriggerOnramp(ctx context.Context, address, phoneNumber, asset string, amount int) (*models.MpesaOnrampResponse, error) {
return &models.MpesaOnrampResponse{}, nil
}