835 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			835 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package http
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 
 | |
| 	"git.defalsify.org/vise.git/logging"
 | |
| 	"git.grassecon.net/grassrootseconomics/sarafu-api/config"
 | |
| 	"git.grassecon.net/grassrootseconomics/sarafu-api/dev"
 | |
| 	"git.grassecon.net/grassrootseconomics/sarafu-api/models"
 | |
| 	"git.grassecon.net/grassrootseconomics/visedriver/storage"
 | |
| 	"github.com/grassrootseconomics/eth-custodial/pkg/api"
 | |
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	aliasRegex = regexp.MustCompile("^\\+?[a-zA-Z0-9\\-_]+$")
 | |
| 	logg       = logging.NewVanilla().WithDomain("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 {
 | |
| 	SS     storage.StorageService
 | |
| 	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:
 | |
| //   - 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
 | |
| //     AccountResponse struct can be used here to check the account status during a transaction.
 | |
| //
 | |
| // Returns:
 | |
| //   - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
 | |
| //   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | |
| //     If no error occurs, this will be nil
 | |
| func (as *HTTPAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
 | |
| 	var r models.TrackStatusResult
 | |
| 
 | |
| 	ep, err := url.JoinPath(config.TrackURL, 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
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) ToFqdn(alias string) string {
 | |
| 	return alias + ".sarafu.eth"
 | |
| }
 | |
| 
 | |
| // CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
 | |
| // Parameters:
 | |
| //   - publicKey: The public key associated with the account whose balance needs to be checked.
 | |
| func (as *HTTPAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
 | |
| 	var balanceResult models.BalanceResult
 | |
| 
 | |
| 	ep, err := url.JoinPath(config.BalanceURL, publicKey)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest("GET", ep, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	_, err = doRequest(ctx, req, &balanceResult)
 | |
| 	return &balanceResult, err
 | |
| }
 | |
| 
 | |
| // CreateAccount creates a new account in the custodial system.
 | |
| // Returns:
 | |
| //   - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
 | |
| //     If there is an error during the request or processing, this will be nil.
 | |
| //   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | |
| //     If no error occurs, this will be nil.
 | |
| func (as *HTTPAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
 | |
| 	var r models.AccountResult
 | |
| 	// Create a new request
 | |
| 	req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &r, nil
 | |
| }
 | |
| 
 | |
| // FetchVouchers retrieves the token holdings for a given public key from the data indexer API endpoint
 | |
| // Parameters:
 | |
| //   - publicKey: The public key associated with the account.
 | |
| func (as *HTTPAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
 | |
| 	var r struct {
 | |
| 		Holdings []dataserviceapi.TokenHoldings `json:"holdings"`
 | |
| 	}
 | |
| 
 | |
| 	ep, err := url.JoinPath(config.VoucherHoldingsURL, 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
 | |
| 	}
 | |
| 
 | |
| 	// Normalize symbols before returning
 | |
| 	for i := range r.Holdings {
 | |
| 		r.Holdings[i].TokenSymbol = sanitizeSymbol(r.Holdings[i].TokenSymbol)
 | |
| 	}
 | |
| 
 | |
| 	return r.Holdings, nil
 | |
| }
 | |
| 
 | |
| // FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint
 | |
| // Parameters:
 | |
| //   - publicKey: The public key associated with the account.
 | |
| func (as *HTTPAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
 | |
| 	var r struct {
 | |
| 		Transfers []dataserviceapi.Last10TxResponse `json:"transfers"`
 | |
| 	}
 | |
| 
 | |
| 	ep, err := url.JoinPath(config.VoucherTransfersURL, 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
 | |
| 	}
 | |
| 
 | |
| 	// Normalize symbols before returning
 | |
| 	for i := range r.Transfers {
 | |
| 		r.Transfers[i].TokenSymbol = sanitizeSymbol(r.Transfers[i].TokenSymbol)
 | |
| 	}
 | |
| 
 | |
| 	return r.Transfers, nil
 | |
| }
 | |
| 
 | |
| // VoucherData retrieves voucher metadata from the data indexer API endpoint.
 | |
| // Parameters:
 | |
| //   - address: The voucher address.
 | |
| func (as *HTTPAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
 | |
| 	var r struct {
 | |
| 		TokenDetails models.VoucherDataResult `json:"tokenDetails"`
 | |
| 	}
 | |
| 
 | |
| 	ep, err := url.JoinPath(config.VoucherDataURL, address)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest("GET", ep, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Normalize symbols before returning
 | |
| 	r.TokenDetails.TokenSymbol = sanitizeSymbol(r.TokenDetails.TokenSymbol)
 | |
| 
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	return &r.TokenDetails, err
 | |
| }
 | |
| 
 | |
| // TokenTransfer creates a new token transfer in the custodial system.
 | |
| // Returns:
 | |
| //   - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId.
 | |
| //     If there is an error during the request or processing, this will be nil.
 | |
| //   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | |
| //     If no error occurs, this will be nil.
 | |
| func (as *HTTPAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
 | |
| 	var r models.TokenTransferResponse
 | |
| 
 | |
| 	// Create request payload
 | |
| 	payload := map[string]string{
 | |
| 		"amount":       amount,
 | |
| 		"from":         from,
 | |
| 		"to":           to,
 | |
| 		"tokenAddress": tokenAddress,
 | |
| 	}
 | |
| 
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Create a new request
 | |
| 	req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &r, nil
 | |
| }
 | |
| 
 | |
| // CheckAliasAddress retrieves the address of an alias from the API endpoint.
 | |
| // Parameters:
 | |
| //   - alias: The alias of the user.
 | |
| func (as *HTTPAccountService) CheckAliasAddress(ctx context.Context, alias string) (*models.AliasAddress, error) {
 | |
| 	if as.SS == nil {
 | |
| 		return nil, fmt.Errorf("The storage service cannot be nil")
 | |
| 	}
 | |
| 	logg.InfoCtxf(ctx, "resolving alias before formatting", "alias", alias)
 | |
| 	svc := dev.NewDevAccountService(ctx, as.SS)
 | |
| 	if as.UseApi {
 | |
| 		logg.InfoCtxf(ctx, "resolving alias to address", "alias", alias)
 | |
| 		return resolveAliasAddress(ctx, alias)
 | |
| 	} else {
 | |
| 		return svc.CheckAliasAddress(ctx, alias)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func resolveAliasAddress(ctx context.Context, alias string) (*models.AliasAddress, error) {
 | |
| 	var aliasEnsResult models.AliasEnsAddressResult
 | |
| 
 | |
| 	fullURL, err := url.JoinPath(config.AliasResolverURL, alias)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest("GET", fullURL, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	_, err = doRequest(ctx, req, &aliasEnsResult)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &models.AliasAddress{Address: aliasEnsResult.Address}, nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) FetchTopPools(ctx context.Context) ([]dataserviceapi.PoolDetails, error) {
 | |
| 	svc := dev.NewDevAccountService(ctx, as.SS)
 | |
| 	if as.UseApi {
 | |
| 		return fetchCustodialTopPools(ctx)
 | |
| 	} else {
 | |
| 		return svc.FetchTopPools(ctx)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func fetchCustodialTopPools(ctx context.Context) ([]dataserviceapi.PoolDetails, error) {
 | |
| 	var r struct {
 | |
| 		TopPools []dataserviceapi.PoolDetails `json:"topPools"`
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest("GET", config.TopPoolsURL, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	return r.TopPools, nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) RetrievePoolDetails(ctx context.Context, sym string) (*dataserviceapi.PoolDetails, error) {
 | |
| 	if as.UseApi {
 | |
| 		return retrievePoolDetails(ctx, sym)
 | |
| 	} else {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func retrievePoolDetails(ctx context.Context, sym string) (*dataserviceapi.PoolDetails, error) {
 | |
| 	var r struct {
 | |
| 		PoolDetails dataserviceapi.PoolDetails `json:"poolDetails"`
 | |
| 	}
 | |
| 
 | |
| 	ep, err := url.JoinPath(config.RetrievePoolDetailsURL, sym)
 | |
| 	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.PoolDetails, nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) PoolDeposit(ctx context.Context, amount, from, poolAddress, tokenAddress string) (*models.PoolDepositResult, error) {
 | |
| 	var r models.PoolDepositResult
 | |
| 
 | |
| 	//pool deposit payload
 | |
| 	payload := map[string]string{
 | |
| 		"amount":       amount,
 | |
| 		"from":         from,
 | |
| 		"poolAddress":  poolAddress,
 | |
| 		"tokenAddress": tokenAddress,
 | |
| 	}
 | |
| 
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return &r, nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) GetPoolSwapQuote(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapQuoteResult, error) {
 | |
| 	var r models.PoolSwapQuoteResult
 | |
| 
 | |
| 	//pool swap quote payload
 | |
| 	payload := map[string]string{
 | |
| 		"amount":           amount,
 | |
| 		"from":             from,
 | |
| 		"fromTokenAddress": fromTokenAddress,
 | |
| 		"poolAddress":      poolAddress,
 | |
| 		"toTokenAddress":   toTokenAddress,
 | |
| 	}
 | |
| 
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest("POST", config.PoolSwapQuoteURL, bytes.NewBuffer(payloadBytes))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return &r, nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) GetPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
 | |
| 	if as.UseApi {
 | |
| 		return as.getPoolSwappableFromVouchers(ctx, poolAddress, publicKey)
 | |
| 	} else {
 | |
| 		svc := dev.NewDevAccountService(ctx, as.SS)
 | |
| 		return svc.GetPoolSwappableFromVouchers(ctx, poolAddress, publicKey)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) getPoolSwappableFromVouchers(ctx context.Context, poolAddress, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
 | |
| 	var r struct {
 | |
| 		PoolSwappableVouchers []dataserviceapi.TokenHoldings `json:"filtered"`
 | |
| 	}
 | |
| 	ep, err := url.JoinPath(config.PoolSwappableVouchersURL, poolAddress, "from", 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
 | |
| 	}
 | |
| 
 | |
| 	// Normalize symbols before returning
 | |
| 	for i := range r.PoolSwappableVouchers {
 | |
| 		r.PoolSwappableVouchers[i].TokenSymbol = sanitizeSymbol(r.PoolSwappableVouchers[i].TokenSymbol)
 | |
| 	}
 | |
| 
 | |
| 	return r.PoolSwappableVouchers, nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) GetPoolSwappableVouchers(ctx context.Context, poolAddress string) ([]dataserviceapi.TokenHoldings, error) {
 | |
| 	svc := dev.NewDevAccountService(ctx, as.SS)
 | |
| 	if as.UseApi {
 | |
| 		return as.getPoolSwappableVouchers(ctx, poolAddress)
 | |
| 	} else {
 | |
| 		return svc.GetPoolSwappableVouchers(ctx, poolAddress)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (as HTTPAccountService) getPoolSwappableVouchers(ctx context.Context, poolAddress string) ([]dataserviceapi.TokenHoldings, error) {
 | |
| 	var r struct {
 | |
| 		PoolSwappableVouchers []dataserviceapi.TokenHoldings `json:"filtered"`
 | |
| 	}
 | |
| 
 | |
| 	basePath, err := url.JoinPath(config.PoolSwappableVouchersURL, poolAddress, "to")
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	parsedURL, err := url.Parse(basePath)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	query := parsedURL.Query()
 | |
| 	if config.IncludeStablesParam != "" {
 | |
| 		query.Set("stables", config.IncludeStablesParam)
 | |
| 	}
 | |
| 	parsedURL.RawQuery = query.Encode()
 | |
| 
 | |
| 	req, err := http.NewRequest("GET", parsedURL.String(), nil)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	_, 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
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) PoolSwap(ctx context.Context, amount, from, fromTokenAddress, poolAddress, toTokenAddress string) (*models.PoolSwapResult, error) {
 | |
| 	var r models.PoolSwapResult
 | |
| 
 | |
| 	//swap payload
 | |
| 	payload := map[string]string{
 | |
| 		"amount":           amount,
 | |
| 		"from":             from,
 | |
| 		"fromTokenAddress": fromTokenAddress,
 | |
| 		"poolAddress":      poolAddress,
 | |
| 		"toTokenAddress":   toTokenAddress,
 | |
| 	}
 | |
| 
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest("POST", config.PoolSwapURL, bytes.NewBuffer(payloadBytes))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return &r, nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) GetSwapFromTokenMaxLimit(ctx context.Context, poolAddress, fromTokenAddress, toTokenAddress, publicKey string) (*models.MaxLimitResult, error) {
 | |
| 	if as.UseApi {
 | |
| 		return as.getSwapFromTokenMaxLimit(ctx, poolAddress, fromTokenAddress, toTokenAddress, publicKey)
 | |
| 	} else {
 | |
| 		svc := dev.NewDevAccountService(ctx, as.SS)
 | |
| 		return svc.GetSwapFromTokenMaxLimit(ctx, poolAddress, fromTokenAddress, toTokenAddress, publicKey)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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, 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
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) CheckTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) {
 | |
| 	if as.UseApi {
 | |
| 		return as.checkTokenInPool(ctx, poolAddress, tokenAddress)
 | |
| 	} else {
 | |
| 		svc := dev.NewDevAccountService(ctx, as.SS)
 | |
| 		return svc.CheckTokenInPool(ctx, poolAddress, tokenAddress)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) checkTokenInPool(ctx context.Context, poolAddress, tokenAddress string) (*models.TokenInPoolResult, error) {
 | |
| 	var r models.TokenInPoolResult
 | |
| 
 | |
| 	ep, err := url.JoinPath(config.PoolSwappableVouchersURL, poolAddress, "check", tokenAddress)
 | |
| 	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: Use actual custodial api to request available alias
 | |
| func (as *HTTPAccountService) RequestAlias(ctx context.Context, publicKey string, hint string) (*models.RequestAliasResult, error) {
 | |
| 	if as.SS == nil {
 | |
| 		return nil, fmt.Errorf("The storage service cannot be nil")
 | |
| 	}
 | |
| 	if as.UseApi {
 | |
| 		if !strings.Contains(hint, ".") {
 | |
| 			hint = as.ToFqdn(hint)
 | |
| 		}
 | |
| 		enr, err := requestEnsAlias(ctx, publicKey, hint)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		return &models.RequestAliasResult{Alias: enr.Name}, nil
 | |
| 	} else {
 | |
| 		svc := dev.NewDevAccountService(ctx, as.SS)
 | |
| 		return svc.RequestAlias(ctx, publicKey, hint)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func requestEnsAlias(ctx context.Context, publicKey string, hint string) (*models.AliasEnsResult, error) {
 | |
| 	var r models.AliasEnsResult
 | |
| 
 | |
| 	endpoint := config.AliasRegistrationURL
 | |
| 
 | |
| 	logg.InfoCtxf(ctx, "requesting alias", "endpoint", endpoint, "hint", hint)
 | |
| 	//Payload with the address and hint to derive an ENS name
 | |
| 	payload := map[string]string{
 | |
| 		"address": publicKey,
 | |
| 		"hint":    hint,
 | |
| 	}
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(payloadBytes))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	// Log the request body
 | |
| 	logg.InfoCtxf(ctx, "request body", "payload", string(payloadBytes))
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	logg.InfoCtxf(ctx, "alias successfully assigned", "alias", r.Name)
 | |
| 	return &r, nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) UpdateAlias(ctx context.Context, name string, publicKey string) (*models.RequestAliasResult, error) {
 | |
| 	if as.SS == nil {
 | |
| 		return nil, fmt.Errorf("The storage service cannot be nil")
 | |
| 	}
 | |
| 	if as.UseApi {
 | |
| 		if !strings.Contains(name, ".") {
 | |
| 			name = as.ToFqdn(name)
 | |
| 		}
 | |
| 		enr, err := updateEnsAlias(ctx, name, publicKey)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		return &models.RequestAliasResult{Alias: enr.Name}, nil
 | |
| 	} else {
 | |
| 		svc := dev.NewDevAccountService(ctx, as.SS)
 | |
| 		return svc.RequestAlias(ctx, publicKey, name)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func updateEnsAlias(ctx context.Context, name string, publicKey string) (*models.AliasEnsResult, error) {
 | |
| 	var r models.AliasEnsResult
 | |
| 
 | |
| 	endpoint := config.AliasUpdateURL
 | |
| 
 | |
| 	logg.InfoCtxf(ctx, "updating alias", "endpoint", endpoint, "name", name)
 | |
| 
 | |
| 	payload := map[string]string{
 | |
| 		"name":    name,
 | |
| 		"address": publicKey,
 | |
| 	}
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	req, err := http.NewRequest("PUT", endpoint, bytes.NewBuffer(payloadBytes))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	// Log the request body
 | |
| 	logg.InfoCtxf(ctx, "request body", "payload", string(payloadBytes))
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	logg.InfoCtxf(ctx, "alias successfully updated", "alias", r.Name)
 | |
| 	return &r, nil
 | |
| }
 | |
| 
 | |
| // SendSMS calls the API to send out an SMS.
 | |
| // Parameters:
 | |
| //   - inviterPhone: The user initiating the SMS.
 | |
| //   - inviteePhone: The number being invited to Sarafu.
 | |
| func (as *HTTPAccountService) SendUpsellSMS(ctx context.Context, inviterPhone, inviteePhone string) (*models.SendSMSResponse, error) {
 | |
| 	var r models.SendSMSResponse
 | |
| 
 | |
| 	// Create request payload
 | |
| 	payload := map[string]string{
 | |
| 		"inviterPhone": inviterPhone,
 | |
| 		"inviteePhone": inviteePhone,
 | |
| 	}
 | |
| 
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Create a new request
 | |
| 	req, err := http.NewRequest("POST", config.SendSMSURL, bytes.NewBuffer(payloadBytes))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	_, err = doRequest(ctx, req, &r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &r, nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) SendAddressSMS(ctx context.Context, publicKey, originPhone string) error {
 | |
| 	ep, err := url.JoinPath(config.ExternalSMSURL, "address")
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	logg.InfoCtxf(ctx, "sending an address sms", "endpoint", ep, "address", publicKey, "origin-phone", originPhone)
 | |
| 	payload := map[string]string{
 | |
| 		"address":     publicKey,
 | |
| 		"originPhone": originPhone,
 | |
| 	}
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	req, err := http.NewRequest("POST", ep, bytes.NewBuffer(payloadBytes))
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	_, err = doRequest(ctx, req, nil)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (as *HTTPAccountService) SendPINResetSMS(ctx context.Context, admin, phone string) error {
 | |
| 	ep, err := url.JoinPath(config.ExternalSMSURL, "pinreset")
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	logg.InfoCtxf(ctx, "sending pin reset sms", "endpoint", ep, "admin", admin, "phone", phone)
 | |
| 	payload := map[string]string{
 | |
| 		"admin": admin,
 | |
| 		"phone": phone,
 | |
| 	}
 | |
| 	payloadBytes, err := json.Marshal(payload)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	req, err := http.NewRequest("POST", ep, bytes.NewBuffer(payloadBytes))
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	_, err = doRequest(ctx, req, nil)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	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
 | |
| 	var errResponse api.ErrResponse
 | |
| 
 | |
| 	req.Header.Set("Authorization", "Bearer "+config.BearerToken)
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 
 | |
| 	// Log request
 | |
| 	logRequestDetails(req)
 | |
| 
 | |
| 	resp, err := http.DefaultClient.Do(req)
 | |
| 	if err != nil {
 | |
| 		log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error())
 | |
| 		errResponse.Description = err.Error()
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	// Read and log response body
 | |
| 	body, err := io.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("Received response for %s: Status Code: %d | Content-Type: %s | Body: %s",
 | |
| 		req.URL, resp.StatusCode, resp.Header.Get("Content-Type"), string(body))
 | |
| 
 | |
| 	if resp.StatusCode >= http.StatusBadRequest {
 | |
| 		if err := json.Unmarshal(body, &errResponse); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		return nil, &APIError{
 | |
| 			Code:        errResponse.ErrCode,
 | |
| 			Description: errResponse.Description,
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err := json.Unmarshal(body, &okResponse); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if len(okResponse.Result) == 0 {
 | |
| 		return nil, errors.New("empty api result")
 | |
| 	}
 | |
| 
 | |
| 	v, err := json.Marshal(okResponse.Result)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	err = json.Unmarshal(v, &rcpt)
 | |
| 	return &okResponse, err
 | |
| }
 | |
| 
 | |
| func logRequestDetails(req *http.Request) {
 | |
| 	var bodyBytes []byte
 | |
| 	contentType := req.Header.Get("Content-Type")
 | |
| 
 | |
| 	if req.Body != nil {
 | |
| 		bodyBytes, _ = io.ReadAll(req.Body)
 | |
| 		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Restore body
 | |
| 	} else {
 | |
| 		bodyBytes = []byte("-")
 | |
| 	}
 | |
| 
 | |
| 	log.Printf("Outgoing Request -> URL: %s | Method: %s | Content-Type: %s | Body: %s",
 | |
| 		req.URL.String(), req.Method, contentType, string(bodyBytes))
 | |
| }
 |