menu-voucherlist #101

Merged
lash merged 63 commits from menu-voucherlist into master 2024-10-25 15:59:47 +02:00
37 changed files with 823 additions and 349 deletions

View File

@ -61,8 +61,8 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn
ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
ls.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin)
ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
@ -89,11 +89,15 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn
ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
return ussdHandlers, nil
}

View File

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"time"
"git.grassecon.net/urdt/ussd/config"
@ -24,6 +25,7 @@ type AccountServiceInterface interface {
CreateAccount(ctx context.Context) (*api.OKResponse, error)
CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error)
TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error)
FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error)
}
type AccountService struct {
@ -169,6 +171,23 @@ func (as *AccountService) CreateAccount(ctx context.Context) (*api.OKResponse, e
return &okResponse, nil
}
// FetchVouchers retrieves the token holdings for a given public key from the custodial holdings API endpoint
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
file, err := os.Open("sample_tokens.json")
if err != nil {
return nil, err
}
defer file.Close()
var holdings models.VoucherHoldingResponse
if err := json.NewDecoder(file).Decode(&holdings); err != nil {
return nil, err
}
return &holdings, nil
}
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
return &api.OKResponse{
Ok: true,
@ -225,3 +244,31 @@ func (tas *TestAccountService) CheckAccountStatus(ctx context.Context, trackingI
}
return trackResponse, nil
}
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
return &models.VoucherHoldingResponse{
Ok: true,
Result: struct {
Holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
} `json:"holdings"`
}{
Holdings: []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}{
{
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF",
TokenDecimals: "6",
Balance: "2745987",
},
},
},
}, nil
}

View File

@ -22,6 +22,8 @@ import (
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/utils"
"gopkg.in/leonelquinteros/gotext.v1"
"git.grassecon.net/urdt/ussd/internal/storage"
)
var (
@ -187,33 +189,6 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte)
return res, nil
}
// SavePin persists the user's PIN choice into the filesystem
func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input)
// Validate that the PIN is a 4-digit number
if !isValidPIN(accountPIN) {
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(accountPIN))
if err != nil {
return res, err
}
return res, nil
}
func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
res := resource.Result{}
_, ok := ctx.Value("SessionId").(string)
@ -232,6 +207,9 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
return res, nil
}
// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_PIN
// during the account creation process
// and during the change PIN process
func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
@ -240,6 +218,7 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input)
@ -249,11 +228,15 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN))
if err != nil {
return res, err
}
return res, nil
}
@ -282,10 +265,10 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
return res, nil
}
// VerifyPin checks whether the confirmation PIN is similar to the account PIN
// If similar, it sets the USERFLAG_PIN_SET flag allowing the user
// VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN
// If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN allowing the user
// to access the main menu
func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
@ -297,12 +280,12 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN)
temporaryPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN)
if err != nil {
return res, err
}
if bytes.Equal(input, AccountPin) {
if bytes.Equal(input, temporaryPin) {
res.FlagSet = []uint32{flag_valid_pin}
res.FlagReset = []uint32{flag_pin_mismatch}
res.FlagSet = append(res.FlagSet, flag_pin_set)
@ -310,6 +293,11 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
res.FlagSet = []uint32{flag_pin_mismatch}
}
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(temporaryPin))
if err != nil {
return res, err
}
return res, nil
}
@ -628,14 +616,12 @@ func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []by
return res, nil
}
// CheckBalance retrieves the balance from the API using the "PublicKey" and sets
// CheckBalance retrieves the balance of the active voucher and sets
// the balance as the result content
func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
@ -646,23 +632,25 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
l.AddDomain("default")
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
// get the active sym and active balance
activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
if db.IsNotFound(err) {
balance := "0.00"
res.Content = l.Get("Balance: %s\n", balance)
return res, nil
Alfred-mk marked this conversation as resolved Outdated
Outdated
Review

If no active sym then the user has no vouchers yet, and then no balance should be displayed, I think?

If no active sym then the user has no vouchers yet, and then no balance should be displayed, I think?

For this, I've added a function that sets the default voucher, (expecting it to be SRF) if no active voucher is set for the user.

This will be loaded on the main.vis, ensuring that one has the default sym if they haven't set their own

For this, I've added a function that sets the default voucher, (expecting it to be SRF) if no active voucher is set for the user. This will be loaded on the main.vis, ensuring that one has the default sym if they haven't set their own
}
lash marked this conversation as resolved Outdated
Outdated
Review

Why should it be 0.00 when error?

Why should it be 0.00 when error?

The 0.00 is a placeholder. I set it like this without a symbol since the error indicates no symbol exists.

I have updated this to check the specific db error

The 0.00 is a placeholder. I set it like this without a symbol since the error indicates no symbol exists. I have updated this to check the specific db error
return res, err
}
activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil {
return res, err
}
balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey))
if err != nil {
return res, nil
}
if !balanceResponse.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_error)
balance := balanceResponse.Result.Balance
res.Content = l.Get("Balance: %s\n", balance)
res.Content = l.Get("Balance: %s\n", fmt.Sprintf("%s %s", activeBal, activeSym))
return res, nil
}
@ -800,15 +788,13 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
balanceResp, err := h.accountService.CheckBalance(ctx, string(publicKey))
activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil {
return res, nil
return res, err
}
balance := balanceResp.Result.Balance
Alfred-mk marked this conversation as resolved Outdated
Outdated
Review

Same here, if there is no symbol there is no balance.

Same here, if there is no symbol there is no balance.
res.Content = balance
res.Content = string(activeBal)
return res, nil
}
@ -817,54 +803,29 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
// it is not more than the current balance.
func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
amountStr := string(input)
var balanceValue float64
balanceRes, err := h.accountService.CheckBalance(ctx, string(publicKey))
balanceStr := balanceRes.Result.Balance
if !balanceRes.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
// retrieve the active balance
activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil {
return res, err
}
res.Content = balanceStr
res.FlagReset = append(res.FlagReset, flag_api_error)
// Parse the balance
balanceParts := strings.Split(balanceStr, " ")
if len(balanceParts) != 2 {
return res, fmt.Errorf("unexpected balance format: %s", balanceStr)
}
balanceValue, err := strconv.ParseFloat(balanceParts[0], 64)
balanceValue, err = strconv.ParseFloat(string(activeBal), 64)
if err != nil {
return res, fmt.Errorf("failed to parse balance: %v", err)
return res, err
}
// Extract numeric part from input
re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`)
matches := re.FindStringSubmatch(strings.TrimSpace(amountStr))
if len(matches) < 2 {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr
return res, nil
}
inputAmount, err := strconv.ParseFloat(matches[1], 64)
// Extract numeric part from the input amount
amountStr := strings.TrimSpace(string(input))
inputAmount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr
@ -877,12 +838,14 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
return res, nil
}
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr))
// Format the amount with 2 decimal places before saving
formattedAmount := fmt.Sprintf("%.2f", inputAmount)
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(formattedAmount))
if err != nil {
return res, err
}
res.Content = fmt.Sprintf("%s", formattedAmount)
return res, nil
}
@ -925,9 +888,16 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
// retrieve the active symbol
activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
return res, err
}
amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT)
res.Content = string(amount)
res.Content = fmt.Sprintf("%s %s", string(amount), string(activeSym))
return res, nil
}
@ -953,7 +923,9 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT)
res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", string(recipient), string(amount), string(sessionId))
activeSym, _ := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
res.Content = l.Get("Your request has been sent. %s will receive %s %s from %s.", string(recipient), string(amount), string(activeSym), string(sessionId))
account_authorized_flag, err := h.flagManager.GetFlag("flag_account_authorized")
if err != nil {
@ -1037,3 +1009,279 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
return res, nil
}
// SetDefaultVoucher retrieves the current vouchers
// and sets the first as the default voucher, if no active voucher is set
func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher")
// check if the user has an active sym
_, err = store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
Alfred-mk marked this conversation as resolved Outdated
Outdated
Review

Can we put this in a separate method please (that can be unit-tested with data only)

Can we put this in a separate method please (that can be unit-tested with data only)
if db.IsNotFound(err) {
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, nil
}
// Fetch vouchers from the API using the public key
vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil {
return res, nil
}
// Return if there is no voucher
if len(vouchersResp.Result.Holdings) == 0 {
res.FlagSet = append(res.FlagSet, flag_no_active_voucher)
return res, nil
}
// Use only the first voucher
firstVoucher := vouchersResp.Result.Holdings[0]
defaultSym := firstVoucher.TokenSymbol
defaultBal := firstVoucher.Balance
// set the active symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(defaultSym))
if err != nil {
return res, err
}
// set the active balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(defaultBal))
if err != nil {
return res, err
}
return res, nil
}
return res, err
}
res.FlagReset = append(res.FlagReset, flag_no_active_voucher)
return res, nil
}
// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores
// them to gdbm
func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
lash marked this conversation as resolved Outdated
Outdated
Review

Now this is code smell; we don't want to use menu selectors in branching here.

But I assume it is done because it is necessary to determine whether we are selecting token or merely navigating laterally? If that is the case, I will make an issue for go-vise to provide whether lateral is indeed the case.

Now this is code smell; we don't want to use menu selectors in branching here. But I assume it is done because it is necessary to determine whether we are selecting token or merely navigating laterally? If that is the case, I will make an issue for go-vise to provide whether lateral is indeed the case.

This was added because the vise runs the function when we're navigating, instead of prioritizing the INCMP and navigate to the specified node

This was added because the vise runs the function when we're navigating, instead of prioritizing the INCMP and navigate to the specified node
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, nil
}
// Fetch vouchers from the API using the public key
vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil {
return res, nil
}
// process voucher data
voucherSymbolList, voucherBalanceList := ProcessVouchers(vouchersResp.Result.Holdings)
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
err = prefixdb.Put(ctx, []byte("sym"), []byte(voucherSymbolList))
if err != nil {
return res, nil
}
Alfred-mk marked this conversation as resolved Outdated
Outdated
Review

Please put in separate, unit-testable method.

Please put in separate, unit-testable method.
err = prefixdb.Put(ctx, []byte("bal"), []byte(voucherBalanceList))
if err != nil {
return res, nil
}
return res, nil
Alfred-mk marked this conversation as resolved Outdated
Outdated
Review

Not a very descriptive prefix?

Not a very descriptive prefix?
}
// ProcessVouchers formats the holdings into symbol and balance lists.
func ProcessVouchers(holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}) (string, string) {
var numberedSymbols, numberedBalances []string
for i, voucher := range holdings {
numberedSymbols = append(numberedSymbols, fmt.Sprintf("%d:%s", i+1, voucher.TokenSymbol))
numberedBalances = append(numberedBalances, fmt.Sprintf("%d:%s", i+1, voucher.Balance))
}
voucherSymbolList := strings.Join(numberedSymbols, "\n")
voucherBalanceList := strings.Join(numberedBalances, "\n")
return voucherSymbolList, voucherBalanceList
}
// GetVoucherList fetches the list of vouchers and formats them
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
// Read vouchers from the store
store := h.userdataStore
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
voucherData, err := prefixdb.Get(ctx, []byte("sym"))
if err != nil {
return res, nil
}
res.Content = string(voucherData)
return res, nil
}
// ViewVoucher retrieves the token holding and balance from the subprefixDB
func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
inputStr := string(input)
if inputStr == "0" || inputStr == "99" {
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
return res, nil
}
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
// Retrieve the voucher symbol list
voucherSymbolList, err := prefixdb.Get(ctx, []byte("sym"))
if err != nil {
return res, fmt.Errorf("failed to retrieve voucher symbol list: %v", err)
}
// Retrieve the voucher balance list
voucherBalanceList, err := prefixdb.Get(ctx, []byte("bal"))
if err != nil {
return res, fmt.Errorf("failed to retrieve voucher balance list: %v", err)
}
// match the voucher symbol and balance with the input
matchedSymbol, matchedBalance := MatchVoucher(inputStr, string(voucherSymbolList), string(voucherBalanceList))
// If a match is found, write the temporary sym and balance
if matchedSymbol != "" && matchedBalance != "" {
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte(matchedSymbol))
if err != nil {
return res, err
}
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte(matchedBalance))
if err != nil {
return res, err
}
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
res.Content = fmt.Sprintf("%s\n%s", matchedSymbol, matchedBalance)
} else {
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher)
}
return res, nil
}
// MatchVoucher finds the matching voucher symbol and balance based on the input.
func MatchVoucher(inputStr string, voucherSymbols, voucherBalances string) (string, string) {
// Split the lists into slices for processing
symbols := strings.Split(voucherSymbols, "\n")
balances := strings.Split(voucherBalances, "\n")
var matchedSymbol, matchedBalance string
for i, symbol := range symbols {
symbolParts := strings.SplitN(symbol, ":", 2)
if len(symbolParts) != 2 {
continue
}
voucherNum := symbolParts[0]
voucherSymbol := symbolParts[1]
// Check if input matches either the number or the symbol
if inputStr == voucherNum || strings.EqualFold(inputStr, voucherSymbol) {
matchedSymbol = voucherSymbol
// Ensure there's a corresponding balance
if i < len(balances) {
matchedBalance = strings.SplitN(balances[i], ":", 2)[1]
}
break
}
}
return matchedSymbol, matchedBalance
}
// SetVoucher retrieves the temporary sym and balance,
// sets them as the active data and
// clears the temporary data
func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
// get the current temporary symbol
temporarySym, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM)
if err != nil {
return res, err
}
// get the current temporary balance
temporaryBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL)
if err != nil {
return res, err
}
// set the active symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(temporarySym))
if err != nil {
return res, err
}
// set the active balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(temporaryBal))
if err != nil {
return res, err
}
// reset the temporary symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte(""))
if err != nil {
return res, err
}
// reset the temporary balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte(""))
if err != nil {
return res, err
}
res.Content = string(temporarySym)
return res, nil
}

View File

@ -218,7 +218,7 @@ func TestSaveFamilyname(t *testing.T) {
mockStore.AssertExpectations(t)
}
func TestSavePin(t *testing.T) {
func TestSaveTemporaryPin(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
mockStore := new(mocks.MockUserDataStore)
if err != nil {
@ -260,10 +260,10 @@ func TestSavePin(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
// Set up the expected behavior of the mock
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(tt.input)).Return(nil)
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(tt.input)).Return(nil)
// Call the method
res, err := h.SavePin(ctx, "save_pin", tt.input)
res, err := h.SaveTemporaryPin(ctx, "save_pin", tt.input)
if err != nil {
t.Error(err)
@ -481,37 +481,6 @@ func TestCheckIdentifier(t *testing.T) {
}
}
func TestMaxAmount(t *testing.T) {
mockStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
// Define test data
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
publicKey := "0xcasgatweksalw1018221"
expectedBalance := &models.BalanceResponse{
Ok: true,
}
// Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", publicKey).Return(expectedBalance, nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
accountService: mockCreateAccountService,
}
// Call the method
res, _ := h.MaxAmount(ctx, "max_amount", []byte("check_balance"))
//Assert that the balance that was set as the result content is what was returned by Check Balance
assert.Equal(t, expectedBalance.Result.Balance, res.Content)
}
func TestGetSender(t *testing.T) {
mockStore := new(mocks.MockUserDataStore)
@ -532,26 +501,30 @@ func TestGetSender(t *testing.T) {
}
func TestGetAmount(t *testing.T) {
mockStore := new(mocks.MockUserDataStore)
mockDataStore := new(mocks.MockUserDataStore)
// Define test data
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
Amount := "0.03CELO"
amount := "0.03"
activeSym := "SRF"
// Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return([]byte(Amount), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(activeSym), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return([]byte(amount), nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
userdataStore: mockDataStore,
}
// Call the method
res, _ := h.GetAmount(ctx, "get_amount", []byte("Getting amount..."))
res, _ := h.GetAmount(ctx, "get_amount", []byte(""))
formattedAmount := fmt.Sprintf("%s %s", amount, activeSym)
//Assert that the retrieved amount is what was set as the content
assert.Equal(t, Amount, res.Content)
assert.Equal(t, formattedAmount, res.Content)
}
@ -980,7 +953,7 @@ func TestVerifyYob(t *testing.T) {
}
}
func TestVerifyPin(t *testing.T) {
func TestVerifyCreatePin(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
@ -1029,7 +1002,7 @@ func TestVerifyPin(t *testing.T) {
},
}
typ := utils.DATA_ACCOUNT_PIN
typ := utils.DATA_TEMPORARY_PIN
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -1037,8 +1010,11 @@ func TestVerifyPin(t *testing.T) {
// Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte(firstSetPin), nil)
// Set up the expected behavior of the mock
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(firstSetPin)).Return(nil)
// Call the method under test
res, err := h.VerifyPin(ctx, "verify_pin", []byte(tt.input))
res, err := h.VerifyCreatePin(ctx, "verify_create_pin", []byte(tt.input))
// Assert that no errors occurred
assert.NoError(t, err)
@ -1317,16 +1293,18 @@ func TestInitiateTransaction(t *testing.T) {
input []byte
Recipient []byte
Amount []byte
ActiveSym []byte
status string
expectedResult resource.Result
}{
{
name: "Test initiate transaction",
Amount: []byte("0.002 CELO"),
Amount: []byte("0.002"),
ActiveSym: []byte("SRF"),
Recipient: []byte("0x12415ass27192"),
expectedResult: resource.Result{
FlagReset: []uint32{account_authorized_flag},
Content: "Your request has been sent. 0x12415ass27192 will receive 0.002 CELO from 254712345678.",
Content: "Your request has been sent. 0x12415ass27192 will receive 0.002 SRF from 254712345678.",
},
},
}
@ -1335,6 +1313,7 @@ func TestInitiateTransaction(t *testing.T) {
// Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return(tt.Amount, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return(tt.Recipient, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return(tt.ActiveSym, nil)
// Call the method under test
res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input)
@ -1464,7 +1443,6 @@ func TestValidateAmount(t *testing.T) {
t.Logf(err.Error())
}
flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount")
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
@ -1480,66 +1458,33 @@ func TestValidateAmount(t *testing.T) {
tests := []struct {
name string
input []byte
publicKey []byte
balanceResponse *models.BalanceResponse
activeBal []byte
balance string
expectedResult resource.Result
}{
{
name: "Test with valid amount",
input: []byte("0.001"),
balanceResponse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
input: []byte("4.10"),
activeBal: []byte("5"),
expectedResult: resource.Result{
Content: "0.001",
FlagReset: []uint32{flag_api_error},
Content: "4.10",
},
},
{
name: "Test with amount larger than balance",
input: []byte("0.02"),
balanceResponse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
name: "Test with amount larger than active balance",
input: []byte("5.02"),
activeBal: []byte("5"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount},
FlagReset: []uint32{flag_api_error},
Content: "0.02",
Content: "5.02",
},
},
{
name: "Test with invalid amount",
name: "Test with invalid amount format",
input: []byte("0.02ms"),
balanceResponse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
activeBal: []byte("5"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount},
FlagReset: []uint32{flag_api_error},
Content: "0.02ms",
},
},
@ -1547,23 +1492,23 @@ func TestValidateAmount(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Mock behavior for active balance retrieval
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL).Return(tt.activeBal, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil)
mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balanceResponse, nil)
// Mock behavior for storing the amount (if valid)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe()
// Call the method under test
res, _ := h.ValidateAmount(ctx, "test_validate_amount", tt.input)
// Assert that no errors occurred
// Assert no errors occurred
assert.NoError(t, err)
//Assert that the account created flag has been set to the result
assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result")
// Assert the result matches the expected result
assert.Equal(t, tt.expectedResult, res, "Expected result should match actual result")
// Assert that expectations were met
// Assert all expectations were met
mockDataStore.AssertExpectations(t)
})
}
}
@ -1627,80 +1572,52 @@ func TestValidateRecipient(t *testing.T) {
}
func TestCheckBalance(t *testing.T) {
sessionId := "session123"
publicKey := "0X13242618721"
fm, _ := NewFlagManager(flagsPath)
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct {
name string
balanceResonse *models.BalanceResponse
sessionId string
publicKey string
activeSym string
activeBal string
expectedResult resource.Result
expectError bool
}{
{
name: "Test when checking a balance is not a success",
balanceResonse: &models.BalanceResponse{
Ok: false,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_api_error},
},
},
{
name: "Test when checking a balance is a success",
balanceResonse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
expectedResult: resource.Result{
Content: "Balance: 0.003 CELO\n",
FlagReset: []uint32{flag_api_error},
},
name: "User with active sym",
sessionId: "session456",
publicKey: "0X98765432109",
activeSym: "ETH",
activeBal: "1.5",
expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
mockState := state.NewState(16)
mockAccountService := new(mocks.MockAccountService)
ctx := context.WithValue(context.Background(), "SessionId", tt.sessionId)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockDataStore,
flagManager: fm.parser,
st: mockState,
accountService: mockCreateAccountService,
accountService: mockAccountService,
}
// Set up the expected behavior of the mock
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResonse, nil)
// Mock for user with active sym
mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(tt.activeSym), nil)
mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_BAL).Return([]byte(tt.activeBal), nil)
// Call the method
res, _ := h.CheckBalance(ctx, "check_balance", []byte(""))
res, err := h.CheckBalance(ctx, "check_balance", []byte(""))
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, res, "Result should match expected output")
}
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
//Assert that the result set to content is what was expected
assert.Equal(t, res, tt.expectedResult, "Result should contain flags set according to user input")
mockAccountService.AssertExpectations(t)
})
}
}
@ -1844,42 +1761,6 @@ func TestVerifyNewPin(t *testing.T) {
}
func TestSaveTemporaryPIn(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
// Create a new instance of UserDataStore
mockStore := new(mocks.MockUserDataStore)
// Define test data
sessionId := "session123"
PIN := "1234"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
// Set up the expected behavior of the mock
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(PIN)).Return(nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
flagManager: fm.parser,
}
// Call the method
res, err := h.SaveTemporaryPin(ctx, "save_temporary_pin", []byte(PIN))
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
// Assert all expectations were met
mockStore.AssertExpectations(t)
}
func TestConfirmPin(t *testing.T) {
sessionId := "session123"
@ -1927,7 +1808,40 @@ func TestConfirmPin(t *testing.T) {
})
}
}
func TestSetVoucher(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
temporarySym := []byte("tempSym")
temporaryBal := []byte("tempBal")
// Set expectations for the mock data store
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_SYM).Return(temporarySym, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_BAL).Return(temporaryBal, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM, temporarySym).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL, temporaryBal).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte("")).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte("")).Return(nil)
h := &Handlers{
userdataStore: mockDataStore,
}
// Call the method under test
res, err := h.SetVoucher(ctx, "someSym", []byte{})
// Assert that no errors occurred
assert.NoError(t, err)
// Assert that the result content is correct
assert.Equal(t, string(temporarySym), res.Content)
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
}
func TestFetchCustodialBalances(t *testing.T) {

View File

@ -32,3 +32,9 @@ func (m *MockAccountService) TrackAccountStatus(ctx context.Context,publicKey st
args := m.Called(publicKey)
return args.Get(0).(*api.OKResponse), args.Error(1)
}
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.VoucherHoldingResponse), args.Error(1)
}

View File

@ -0,0 +1,18 @@
package models
type ApiResponse struct {
Review

These should be gotten from the api provider go code instead, #106

Since we are introducing new ones here, may as well go directly to the source.

These should be gotten from the api provider go code instead, https://git.grassecon.net/urdt/ussd/issues/106 Since we are introducing new ones here, may as well go directly to the source.
OK bool `json:"ok"`
Description string `json:"description"`
Result Result `json:"result"`
}
type Result struct {
Holdings []Holding `json:"holdings"`
}
type Holding struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}

View File

@ -0,0 +1,15 @@
package models
// VoucherHoldingResponse represents a single voucher holding
type VoucherHoldingResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result struct {
Holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
} `json:"holdings"`
} `json:"result"`
}

View File

@ -22,11 +22,11 @@ func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
}
}
func(s *SubPrefixDb) toKey(k []byte) []byte {
func (s *SubPrefixDb) toKey(k []byte) []byte {
return append(s.pfx, k...)
}
func(s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
v, err := s.store.Get(ctx, key)
@ -36,7 +36,7 @@ func(s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
return v, nil
}
func(s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
return s.store.Put(ctx, key, val)

View File

@ -23,6 +23,11 @@ const (
DATA_RECIPIENT
DATA_AMOUNT
DATA_TEMPORARY_PIN
DATA_VOUCHER_LIST
DATA_TEMPORARY_SYM
DATA_ACTIVE_SYM
DATA_TEMPORARY_BAL
DATA_ACTIVE_BAL
)
func typToBytes(typ DataTyp) []byte {

View File

@ -5,7 +5,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -33,7 +33,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -42,7 +42,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -70,7 +70,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -79,7 +79,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -116,7 +116,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -125,7 +125,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -162,7 +162,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -171,7 +171,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -203,7 +203,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -212,7 +212,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -244,7 +244,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
@ -254,7 +254,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -286,7 +286,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -295,7 +295,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -327,7 +327,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -336,7 +336,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -368,7 +368,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -377,7 +377,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -409,7 +409,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -418,7 +418,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@ -446,7 +446,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}

View File

@ -43,6 +43,39 @@ func extractPublicKey(response []byte) string {
return ""
}
// Extracts the balance value from the engine response.
func extractBalance(response []byte) string {
// Regex to match "Balance: <amount> <symbol>" followed by a newline
re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
}
return ""
}
// Extracts the Maximum amount value from the engine response.
func extractMaxAmount(response []byte) string {
// Regex to match "Maximum amount: <amount>" followed by a newline
re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) // "<amount>"
}
return ""
}
// Extracts the send amount value from the engine response.
func extractSendAmount(response []byte) string {
// Regex to match the pattern "will receive X.XX SYM from"
re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) // Returns "X.XX SYM"
}
return ""
}
func TestMain(m *testing.M) {
sessionID = GenerateSessionId()
defer func() {
@ -154,6 +187,12 @@ func TestMainMenuHelp(t *testing.T) {
}
b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
@ -189,6 +228,12 @@ func TestMainMenuQuit(t *testing.T) {
}
b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
@ -225,8 +270,13 @@ func TestMyAccount_MyAddress(t *testing.T) {
}
b := w.Bytes()
balance := extractBalance(b)
publicKey := extractPublicKey(b)
expectedContent := bytes.Replace([]byte(step.ExpectedContent), []byte("{public_key}"), []byte(publicKey), -1)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
@ -240,6 +290,52 @@ func TestMyAccount_MyAddress(t *testing.T) {
}
}
func TestMainMenuSend(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
max_amount := extractMaxAmount(b)
send_amount := extractSendAmount(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestGroups(t *testing.T) {
groups, err := driver.LoadTestGroups(groupTestFile)
if err != nil {
@ -265,6 +361,13 @@ func TestGroups(t *testing.T) {
t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(tt.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
tt.ExpectedContent = string(expectedContent)
match, err := tt.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err)
@ -272,7 +375,6 @@ func TestGroups(t *testing.T) {
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b)
}
})
}
}

View File

@ -57,7 +57,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "1",
@ -73,19 +73,19 @@
},
{
"input": "065656",
"expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back"
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
},
{
"input": "0.1",
"expectedContent": "Amount 0.1 is invalid, please try again:\n1:retry\n9:Quit"
"input": "10000000",
"expectedContent": "Amount 10000000 is invalid, please try again:\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back"
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
},
{
"input": "0.001",
"expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
"input": "1.00",
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1222",
@ -93,11 +93,11 @@
},
{
"input": "1",
"expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1234",
"expectedContent": "Your request has been sent. 065656 will receive 0.001 from {public_key}."
"expectedContent": "Your request has been sent. 065656 will receive {send_amount} from {session_id}."
}
]
},
@ -106,7 +106,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "4",
@ -119,7 +119,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "9",
@ -132,7 +132,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",

44
sample_tokens.json Normal file
View File

@ -0,0 +1,44 @@
{
"ok": true,
"description": "Token holdings with current balances",
"result": {
"holdings": [
{
"contractAddress": "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
"tokenSymbol": "FSPTST",
"tokenDecimals": "6",
"balance": "8869964242"
},
{
"contractAddress": "0x724F2910D790B54A39a7638282a45B1D83564fFA",
"tokenSymbol": "GEO",
"tokenDecimals": "6",
"balance": "9884"
},
{
"contractAddress": "0x2105a206B7bec31E2F90acF7385cc8F7F5f9D273",
"tokenSymbol": "MFNK",
"tokenDecimals": "6",
"balance": "19788697"
},
{
"contractAddress": "0x63DE2Ac8D1008351Cc69Fb8aCb94Ba47728a7E83",
"tokenSymbol": "MILO",
"tokenDecimals": "6",
"balance": "75"
},
{
"contractAddress": "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
"tokenSymbol": "SOHAIL",
"tokenDecimals": "6",
"balance": "27874115"
},
{
"contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958",
"tokenSymbol": "SRF",
"tokenDecimals": "6",
"balance": "2745987"
}
]
}
}

View File

@ -1,4 +1,4 @@
RELOAD verify_pin
RELOAD verify_create_pin
CATCH create_pin_mismatch flag_pin_mismatch 1
LOAD quit 0
HALT

View File

@ -1,5 +1,6 @@
LOAD reset_transaction_amount 0
LOAD max_amount 10
RELOAD max_amount
MAP max_amount
MOUT back 0
HALT
@ -10,5 +11,5 @@ CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0
LOAD get_recipient 12
LOAD get_sender 64
LOAD get_amount 12
LOAD get_amount 32
INCMP transaction_pin *

View File

@ -1,4 +1,4 @@
LOAD save_pin 0
LOAD save_temporary_pin 6
HALT
LOAD verify_pin 8
LOAD verify_create_pin 8
INCMP account_creation *

View File

@ -2,8 +2,8 @@ LOAD create_account 0
CATCH account_creation_failed flag_account_creation_failed 1
MOUT exit 0
HALT
LOAD save_pin 0
RELOAD save_pin
LOAD save_temporary_pin 6
RELOAD save_temporary_pin
CATCH . flag_incorrect_pin 1
INCMP quit 0
INCMP confirm_create_pin *

View File

@ -1,8 +1,8 @@
msgid "Your account balance is %s"
msgstr "Salio lako ni %s"
msgid "Your request has been sent. %s will receive %s from %s."
msgstr "Ombi lako limetumwa. %s atapokea %s kutoka kwa %s."
msgid "Your request has been sent. %s will receive %s %s from %s."
msgstr "Ombi lako limetumwa. %s atapokea %s %s kutoka kwa %s."
msgid "Thank you for using Sarafu. Goodbye!"
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"

View File

@ -1,5 +1,9 @@
LOAD set_default_voucher 8
RELOAD set_default_voucher
LOAD check_balance 64
RELOAD check_balance
LOAD check_vouchers 10
RELOAD check_vouchers
CATCH api_failure flag_api_call_error 1
MAP check_balance
MOUT send 1
@ -9,7 +13,7 @@ MOUT help 4
MOUT quit 9
HALT
INCMP send 1
INCMP quit 2
INCMP my_vouchers 2
INCMP my_account 3
INCMP help 4
INCMP quit 9

View File

@ -0,0 +1 @@
My vouchers

View File

@ -0,0 +1,8 @@
LOAD reset_account_authorized 16
RELOAD reset_account_authorized
MOUT select_voucher 1
MOUT voucher_details 2
MOUT back 0
HALT
INCMP _ 0
INCMP select_voucher 1

View File

@ -0,0 +1 @@
You need a voucher to send

View File

@ -0,0 +1,5 @@
MOUT back 0
MOUT quit 9
HALT
INCMP ^ 0
INCMP quit 9

View File

@ -0,0 +1 @@
Unahitaji sarafu kutuma

View File

@ -14,4 +14,6 @@ flag,flag_valid_pin,20,this is set when the given PIN is valid
flag,flag_allow_update,21,this is set to allow a user to update their profile data
flag,flag_single_edit,22,this is set to allow a user to edit a single profile item such as year of birth
flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid
flag,flag_incorrect_voucher,24,this is set when the selected voucher is invalid
flag,flag_api_call_error,25,this is set when communication to an external service fails
flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher

1 flag flag_language_set 8 checks whether the user has set their prefered language
14 flag flag_allow_update 21 this is set to allow a user to update their profile data
15 flag flag_single_edit 22 this is set to allow a user to edit a single profile item such as year of birth
16 flag flag_incorrect_date_format 23 this is set when the given year of birth is invalid
17 flag flag_incorrect_voucher 24 this is set when the selected voucher is invalid
18 flag flag_api_call_error 25 this is set when communication to an external service fails this is set when communication to an external service fails
19 flag flag_no_active_voucher 26 this is set when a user does not have an active voucher

View File

@ -0,0 +1,2 @@
Select number or symbol from your vouchers:
{{.get_vouchers}}

View File

@ -0,0 +1,15 @@
LOAD get_vouchers 0
MAP get_vouchers
MOUT back 0
MOUT quit 99
MNEXT next 11
MPREV prev 22
HALT
LOAD view_voucher 80
RELOAD view_voucher
CATCH . flag_incorrect_voucher 1
INCMP _ 0
INCMP quit 99
INCMP > 11
INCMP < 22
INCMP view_voucher *

View File

@ -0,0 +1 @@
Select voucher

View File

@ -0,0 +1,2 @@
Chagua nambari au ishara kutoka kwa salio zako:
{{.get_vouchers}}

View File

@ -1,4 +1,5 @@
LOAD transaction_reset 0
CATCH no_voucher flag_no_active_voucher 1
MOUT back 0
HALT
LOAD validate_recipient 20

View File

@ -0,0 +1,2 @@
Enter PIN to confirm selection:
{{.view_voucher}}

View File

@ -0,0 +1,10 @@
MAP view_voucher
MOUT back 0
MOUT quit 9
LOAD authorize_account 6
HALT
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
INCMP _ 0
INCMP quit 9
INCMP voucher_set *

View File

@ -0,0 +1,2 @@
Weka PIN ili kuthibitisha chaguo:
{{.view_voucher}}

View File

@ -0,0 +1 @@
Voucher details

View File

@ -0,0 +1 @@
Success! {{.set_voucher}} is now your active voucher.

View File

@ -0,0 +1,10 @@
LOAD reset_incorrect 6
CATCH incorrect_pin flag_incorrect_pin 1
CATCH _ flag_account_authorized 0
LOAD set_voucher 12
MAP set_voucher
MOUT back 0
MOUT quit 9
HALT
INCMP ^ 0
INCMP quit 9

View File

@ -0,0 +1 @@
Hongera! {{.set_voucher}} ni Sarafu inayotumika sasa.