Compare commits
5 Commits
8421feb757
...
a766f0c01f
Author | SHA1 | Date | |
---|---|---|---|
a766f0c01f | |||
1a3426e729 | |||
9ba8c040c0 | |||
1f830657f9 | |||
ec0bbc8aaa |
48
common/recipient.go
Normal file
48
common/recipient.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Define the regex patterns as constants
|
||||||
|
const (
|
||||||
|
phoneRegex = `^(?:\+254|254|0)?((?:7[0-9]{8})|(?:1[01][0-9]{7}))$`
|
||||||
|
addressRegex = `^0x[a-fA-F0-9]{40}$`
|
||||||
|
aliasRegex = `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$`
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValidPhoneNumber checks if the given number is a valid phone number
|
||||||
|
func IsValidPhoneNumber(phonenumber string) bool {
|
||||||
|
match, _ := regexp.MatchString(phoneRegex, phonenumber)
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidAddress checks if the given address is a valid Ethereum address
|
||||||
|
func IsValidAddress(address string) bool {
|
||||||
|
match, _ := regexp.MatchString(addressRegex, address)
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidAlias checks if the alias is a valid alias format
|
||||||
|
func IsValidAlias(alias string) bool {
|
||||||
|
match, _ := regexp.MatchString(aliasRegex, alias)
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckRecipient validates the recipient format based on the criteria
|
||||||
|
func CheckRecipient(recipient string) (string, error) {
|
||||||
|
if IsValidPhoneNumber(recipient) {
|
||||||
|
return "phone number", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsValidAddress(recipient) {
|
||||||
|
return "address", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsValidAlias(recipient) {
|
||||||
|
return "alias", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("invalid recipient: must be a phone number, address or alias")
|
||||||
|
}
|
@ -15,6 +15,7 @@ const (
|
|||||||
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"
|
||||||
|
AliasPrefix = "api/v1/alias"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -32,6 +33,7 @@ var (
|
|||||||
VoucherHoldingsURL string
|
VoucherHoldingsURL string
|
||||||
VoucherTransfersURL string
|
VoucherTransfersURL string
|
||||||
VoucherDataURL string
|
VoucherDataURL string
|
||||||
|
CheckAliasURL string
|
||||||
)
|
)
|
||||||
|
|
||||||
func setBase() error {
|
func setBase() error {
|
||||||
@ -66,6 +68,7 @@ func LoadConfig() error {
|
|||||||
VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix)
|
VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix)
|
||||||
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
|
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
|
||||||
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
|
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
|
||||||
|
CheckAliasURL, _ = url.JoinPath(custodialURLBase, AliasPrefix)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -12,7 +12,7 @@ require (
|
|||||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect
|
require github.com/grassrootseconomics/ussd-data-service v1.2.0-beta // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -20,6 +20,8 @@ github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQ
|
|||||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo=
|
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo=
|
||||||
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU=
|
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU=
|
||||||
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw=
|
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw=
|
||||||
|
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk=
|
||||||
|
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0=
|
||||||
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
|
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
|
||||||
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
|
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
@ -35,12 +35,17 @@ var (
|
|||||||
errResponse *api.ErrResponse
|
errResponse *api.ErrResponse
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define the regex patterns as constants
|
// Define the regex patterns as constants
|
||||||
const (
|
const (
|
||||||
phoneRegex = `^(?:\+254|254|0)?((?:7[0-9]{8})|(?:1[01][0-9]{7}))$`
|
|
||||||
pinPattern = `^\d{4}$`
|
pinPattern = `^\d{4}$`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// isValidPIN checks whether the given input is a 4 digit number
|
||||||
|
func isValidPIN(pin string) bool {
|
||||||
|
match, _ := regexp.MatchString(pinPattern, pin)
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
// FlagManager handles centralized flag management
|
// FlagManager handles centralized flag management
|
||||||
type FlagManager struct {
|
type FlagManager struct {
|
||||||
parser *asm.FlagParser
|
parser *asm.FlagParser
|
||||||
@ -95,17 +100,6 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
|
|||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidPIN checks whether the given input is a 4 digit number
|
|
||||||
func isValidPIN(pin string) bool {
|
|
||||||
match, _ := regexp.MatchString(pinPattern, pin)
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidPhoneNumber(phonenumber string) bool {
|
|
||||||
match, _ := regexp.MatchString(phoneRegex, phonenumber)
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
|
func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
|
||||||
if h.pe != nil {
|
if h.pe != nil {
|
||||||
panic("persister already set")
|
panic("persister already set")
|
||||||
@ -877,7 +871,7 @@ func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input
|
|||||||
}
|
}
|
||||||
blockedNumber := string(input)
|
blockedNumber := string(input)
|
||||||
_, err = store.ReadEntry(ctx, blockedNumber, common.DATA_PUBLIC_KEY)
|
_, err = store.ReadEntry(ctx, blockedNumber, common.DATA_PUBLIC_KEY)
|
||||||
if !isValidPhoneNumber(blockedNumber) {
|
if !common.IsValidPhoneNumber(blockedNumber) {
|
||||||
res.FlagSet = append(res.FlagSet, flag_unregistered_number)
|
res.FlagSet = append(res.FlagSet, flag_unregistered_number)
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
@ -898,10 +892,9 @@ func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRecipient validates that the given input is a valid phone number.
|
// ValidateRecipient validates that the given input is valid.
|
||||||
func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||||
var res resource.Result
|
var res resource.Result
|
||||||
var err error
|
|
||||||
store := h.userdataStore
|
store := h.userdataStore
|
||||||
|
|
||||||
sessionId, ok := ctx.Value("SessionId").(string)
|
sessionId, ok := ctx.Value("SessionId").(string)
|
||||||
@ -909,13 +902,16 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by
|
|||||||
return res, fmt.Errorf("missing session")
|
return res, fmt.Errorf("missing session")
|
||||||
}
|
}
|
||||||
|
|
||||||
recipient := string(input)
|
|
||||||
|
|
||||||
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
|
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
|
||||||
flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite")
|
flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite")
|
||||||
|
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
|
||||||
|
|
||||||
|
recipient := string(input)
|
||||||
|
|
||||||
if recipient != "0" {
|
if recipient != "0" {
|
||||||
if !isValidPhoneNumber(recipient) {
|
recipientType, err := common.CheckRecipient(recipient)
|
||||||
|
if err != nil {
|
||||||
|
// Invalid recipient format (not a phone number, address, or valid alias format)
|
||||||
res.FlagSet = append(res.FlagSet, flag_invalid_recipient)
|
res.FlagSet = append(res.FlagSet, flag_invalid_recipient)
|
||||||
res.Content = recipient
|
res.Content = recipient
|
||||||
|
|
||||||
@ -929,25 +925,54 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
publicKey, err := store.ReadEntry(ctx, recipient, common.DATA_PUBLIC_KEY)
|
switch recipientType {
|
||||||
if err != nil {
|
case "phone number":
|
||||||
if db.IsNotFound(err) {
|
// Check if the phone number is registered
|
||||||
logg.InfoCtxf(ctx, "Unregistered number")
|
publicKey, err := store.ReadEntry(ctx, recipient, common.DATA_PUBLIC_KEY)
|
||||||
|
if err != nil {
|
||||||
|
if db.IsNotFound(err) {
|
||||||
|
logg.InfoCtxf(ctx, "Unregistered phone number: %s", recipient)
|
||||||
|
res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite)
|
||||||
|
res.Content = recipient
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite)
|
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err)
|
||||||
res.Content = recipient
|
return res, err
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err)
|
// Save the publicKey as the recipient
|
||||||
return res, err
|
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, publicKey)
|
||||||
}
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", string(publicKey), "error", err)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, publicKey)
|
case "address":
|
||||||
if err != nil {
|
// Save the valid Ethereum address as the recipient
|
||||||
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", string(publicKey), "error", err)
|
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(recipient))
|
||||||
return res, nil
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", recipient, "error", err)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case "alias":
|
||||||
|
// Call the API to validate and retrieve the address for the alias
|
||||||
|
r, aliasErr := h.accountService.CheckAliasAddress(ctx, recipient)
|
||||||
|
if aliasErr != nil {
|
||||||
|
res.FlagSet = append(res.FlagSet, flag_api_error)
|
||||||
|
res.Content = recipient
|
||||||
|
|
||||||
|
logg.ErrorCtxf(ctx, "failed on CheckAliasAddress", "error", aliasErr)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alias validation succeeded, save the Ethereum address
|
||||||
|
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(r.Address))
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", r.Address, "error", err)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,3 +47,8 @@ func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to
|
|||||||
args := m.Called()
|
args := m.Called()
|
||||||
return args.Get(0).(*models.TokenTransferResponse), args.Error(1)
|
return args.Get(0).(*models.TokenTransferResponse), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1)
|
||||||
|
}
|
||||||
|
@ -33,8 +33,8 @@ 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 {
|
dataserviceapi.TokenHoldings{
|
||||||
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
|
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
|
||||||
TokenSymbol: "SRF",
|
TokenSymbol: "SRF",
|
||||||
TokenDecimals: "6",
|
TokenDecimals: "6",
|
||||||
@ -56,3 +56,7 @@ func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from,
|
|||||||
TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af",
|
TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
|
||||||
|
return &dataserviceapi.AliasAddress{}, nil
|
||||||
|
}
|
||||||
|
@ -24,6 +24,7 @@ type AccountServiceInterface interface {
|
|||||||
FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error)
|
FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error)
|
||||||
VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error)
|
VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error)
|
||||||
TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error)
|
TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error)
|
||||||
|
CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountService struct {
|
type AccountService struct {
|
||||||
@ -209,6 +210,26 @@ func (as *AccountService) TokenTransfer(ctx context.Context, amount, from, to, t
|
|||||||
return &r, nil
|
return &r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckAliasAddress retrieves the address of an alias from the API endpoint.
|
||||||
|
// Parameters:
|
||||||
|
// - alias: The alias of the user.
|
||||||
|
func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
|
||||||
|
var r dataserviceapi.AliasAddress
|
||||||
|
|
||||||
|
ep, err := url.JoinPath(config.CheckAliasURL, alias)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", ep, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = doRequest(ctx, req, &r)
|
||||||
|
return &r, err
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
var errResponse api.ErrResponse
|
var errResponse api.ErrResponse
|
||||||
|
Loading…
Reference in New Issue
Block a user