Compare commits

..

No commits in common. "master" and "exclude-active-data-from-lists" have entirely different histories.

54 changed files with 271 additions and 982 deletions

View File

@ -21,7 +21,7 @@ RUN make VISE_PATH=/build/go-vise -B
WORKDIR /build/sarafu-vise
RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM"
RUN go mod download
RUN go build -tags logtrace,online -o sarafu-at -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
RUN go build -tags logdebug,online -o sarafu-at -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
FROM debian:bookworm-slim

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.23.4
require (
git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66
git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20250417111317-2953f4c2f32e
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028083421-fe897cca84f2
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630214912-814bef2b209a
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306
git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694
github.com/alecthomas/assert/v2 v2.2.2

8
go.sum
View File

@ -20,14 +20,6 @@ git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630213606-
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630213606-12940bb5f284/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630214912-814bef2b209a h1:KuhJ/WY4RCGmrXUA680ciaponM4vM5zBOJfnCpUo2fc=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630214912-814bef2b209a/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251021120522-6f7802b58cf5 h1:bQglHVxMilciZ9M2PGuLgA+Wkvqo8OqQh6TFYwjtuSE=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251021120522-6f7802b58cf5/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63 h1:yznaUXeAy+qiZb2nCxosYXE5HyCPpynIoplEuYV/zQM=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028081048-a705443786fd h1:VIj5OdRae2wfE6NdLp6ZdHv0jtRbOeRURYQCU29RWBM=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028081048-a705443786fd/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028083421-fe897cca84f2 h1:wf//obTSLW5VZ0gM25l0U5oV/d+TBXX+1ClSMkEU7Uc=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028083421-fe897cca84f2/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306 h1:Jo+yWysWw/N5BJQtAyEMN8ePVvAyPHv+JG4lQti+1N4=
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306/go.mod h1:FdLwYtzsjOIcDiW4uDgDYnB4Wdzq12uJUe0QHSSPbSo=
git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 h1:DjJlBSz0S13acft5XZDWk7ZYnzElym0xLMYEVgyNJ+E=

View File

@ -83,7 +83,6 @@ func (h *MenuHandlers) CheckAccountCreated(ctx context.Context, sym string, inpu
// CheckBlockedStatus:
// 1. Checks whether the DATA_SELF_PIN_RESET is 1 and sets the flag_account_pin_reset
// 2. resets the account blocked flag if the PIN attempts have been reset by an admin.
// 3. Sets key flags (language and PIN) if the data exists
func (h *MenuHandlers) CheckBlockedStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
store := h.userdataStore
@ -91,30 +90,11 @@ func (h *MenuHandlers) CheckBlockedStatus(ctx context.Context, sym string, input
flag_account_blocked, _ := h.flagManager.GetFlag("flag_account_blocked")
flag_account_pin_reset, _ := h.flagManager.GetFlag("flag_account_pin_reset")
flag_pin_set, _ := h.flagManager.GetFlag("flag_pin_set")
flag_language_set, _ := h.flagManager.GetFlag("flag_language_set")
pinFlagSet := h.st.MatchFlag(flag_pin_set, true)
languageFlagSet := h.st.MatchFlag(flag_language_set, true)
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
// only check the data if the flag isn't set
if !pinFlagSet {
accountPin, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_ACCOUNT_PIN)
if len(accountPin) > 0 {
res.FlagSet = append(res.FlagSet, flag_pin_set)
}
}
if !languageFlagSet {
languageCode, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_SELECTED_LANGUAGE_CODE)
if len(languageCode) > 0 {
res.FlagSet = append(res.FlagSet, flag_language_set)
}
}
res.FlagReset = append(res.FlagReset, flag_account_pin_reset)
selfPinReset, err := store.ReadEntry(ctx, sessionId, storedb.DATA_SELF_PIN_RESET)

View File

@ -5,7 +5,6 @@ import (
"testing"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state"
"git.grassecon.net/grassrootseconomics/sarafu-api/models"
"git.grassecon.net/grassrootseconomics/sarafu-api/testutil/mocks"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
@ -94,24 +93,16 @@ func TestCheckBlockedStatus(t *testing.T) {
}
flag_account_blocked, _ := fm.GetFlag("flag_account_blocked")
flag_account_pin_reset, _ := fm.GetFlag("flag_account_pin_reset")
flag_pin_set, _ := fm.GetFlag("flag_pin_set")
flag_language_set, _ := fm.GetFlag("flag_language_set")
// Set the flag in the State
mockState := state.NewState(128)
h := &MenuHandlers{
userdataStore: store,
flagManager: fm,
st: mockState,
}
tests := []struct {
name string
currentWrongPinAttempts string
expectedResult resource.Result
languageSet bool
PinSet bool
}{
{
name: "Currently blocked account",
@ -127,16 +118,6 @@ func TestCheckBlockedStatus(t *testing.T) {
FlagReset: []uint32{flag_account_pin_reset, flag_account_blocked},
},
},
{
name: "Valid account with reset language and PIN flags",
currentWrongPinAttempts: "0",
languageSet: true,
PinSet: true,
expectedResult: resource.Result{
FlagReset: []uint32{flag_account_pin_reset, flag_account_blocked},
FlagSet: []uint32{flag_pin_set, flag_language_set},
},
},
}
for _, tt := range tests {
@ -145,18 +126,6 @@ func TestCheckBlockedStatus(t *testing.T) {
t.Fatal(err)
}
if tt.languageSet {
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_SELECTED_LANGUAGE_CODE, []byte("eng")); err != nil {
t.Fatal(err)
}
}
if tt.PinSet {
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_ACCOUNT_PIN, []byte("hasedPinValue")); err != nil {
t.Fatal(err)
}
}
res, err := h.CheckBlockedStatus(ctx, "", []byte(""))
assert.NoError(t, err)

View File

@ -52,7 +52,7 @@ func TestCheckBalance(t *testing.T) {
alias: "user72",
activeSym: "SRF",
activeBal: "10.967",
expectedResult: resource.Result{Content: "user72\nBalance: 10.96 SRF\n"},
expectedResult: resource.Result{Content: "user72 balance: 10.96 SRF\n"},
expectError: false,
},
}

View File

@ -22,12 +22,12 @@ func (h *MenuHandlers) GetPools(ctx context.Context, sym string, input []byte) (
}
userStore := h.userdataStore
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_error")
// call the api to get a list of top 5 pools sorted by swaps
topPools, err := h.accountService.FetchTopPools(ctx)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err)
return res, err
}
@ -129,12 +129,12 @@ func (h *MenuHandlers) ViewPool(ctx context.Context, sym string, input []byte) (
}
if poolData == nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
// no match found. Call the API using the inputStr as the symbol
poolResp, err := h.accountService.RetrievePoolDetails(ctx, inputStr)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}

View File

@ -41,7 +41,7 @@ func (h *MenuHandlers) LoadSwapToList(ctx context.Context, sym string, input []b
l.AddDomain("default")
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_error")
inputStr := string(input)
if inputStr == "0" {
@ -88,7 +88,7 @@ func (h *MenuHandlers) LoadSwapToList(ctx context.Context, sym string, input []b
// call the api using the ActivePoolAddress and ActiveVoucherAddress to check if it is part of the pool
r, err := h.accountService.CheckTokenInPool(ctx, string(activePoolAddress), string(activeAddress))
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed on CheckTokenInPool", "error", err)
return res, err
}
@ -110,7 +110,7 @@ func (h *MenuHandlers) LoadSwapToList(ctx context.Context, sym string, input []b
// call the api using the activePoolAddress to get a list of SwapToSymbolsData
swapToList, err := h.accountService.GetPoolSwappableVouchers(ctx, string(activePoolAddress))
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err)
return res, err
}
@ -165,7 +165,7 @@ func (h *MenuHandlers) SwapMaxLimit(ctx context.Context, sym string, input []byt
}
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_error")
flag_low_swap_amount, _ := h.flagManager.GetFlag("flag_low_swap_amount")
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher, flag_low_swap_amount)
@ -202,9 +202,9 @@ func (h *MenuHandlers) SwapMaxLimit(ctx context.Context, sym string, input []byt
logg.InfoCtxf(ctx, "Call GetSwapFromTokenMaxLimit with:", "ActivePoolAddress", swapData.ActivePoolAddress, "ActiveSwapFromAddress", swapData.ActiveSwapFromAddress, "ActiveSwapToAddress", swapData.ActiveSwapToAddress, "publicKey", swapData.PublicKey)
r, err := h.accountService.GetSwapFromTokenMaxLimit(ctx, swapData.ActivePoolAddress, swapData.ActiveSwapFromAddress, swapData.ActiveSwapToAddress, swapData.PublicKey)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed on GetSwapFromTokenMaxLimit", "error", err)
return res, nil
return res, err
}
// Scale down the amount
@ -220,7 +220,7 @@ func (h *MenuHandlers) SwapMaxLimit(ctx context.Context, sym string, input []byt
}
// Format to 2 decimal places
maxStr, _ := store.TruncateDecimalString(string(maxAmountStr), 2)
maxStr := fmt.Sprintf("%.2f", maxAmountFloat)
if maxAmountFloat < 0.1 {
// return with low amount flag
@ -310,8 +310,8 @@ func (h *MenuHandlers) SwapPreview(ctx context.Context, sym string, input []byte
// call the API to get the quote
r, err := h.accountService.GetPoolSwapQuote(ctx, finalAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil
@ -319,9 +319,14 @@ func (h *MenuHandlers) SwapPreview(ctx context.Context, sym string, input []byte
// Scale down the quoted amount
quoteAmountStr := store.ScaleDownBalance(r.OutValue, swapData.ActiveSwapToDecimal)
qouteAmount, err := strconv.ParseFloat(quoteAmountStr, 64)
if err != nil {
logg.ErrorCtxf(ctx, "failed to parse quoteAmountStr as float", "value", quoteAmountStr, "error", err)
return res, err
}
// Format to 2 decimal places
qouteStr, _ := store.TruncateDecimalString(string(quoteAmountStr), 2)
qouteStr := fmt.Sprintf("%.2f", qouteAmount)
res.Content = fmt.Sprintf(
"You will swap:\n%s %s for %s %s:",
@ -364,8 +369,8 @@ func (h *MenuHandlers) InitiateSwap(ctx context.Context, sym string, input []byt
// Call the poolSwap API
r, err := h.accountService.PoolSwap(ctx, swapAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil

View File

@ -2,246 +2,140 @@ package application
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/common/hex"
"git.grassecon.net/grassrootseconomics/common/identity"
"git.grassecon.net/grassrootseconomics/common/phone"
"git.grassecon.net/grassrootseconomics/sarafu-api/remote/http"
"git.grassecon.net/grassrootseconomics/sarafu-api/models"
"git.grassecon.net/grassrootseconomics/sarafu-vise/config"
"git.grassecon.net/grassrootseconomics/sarafu-vise/store"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
"github.com/grassrootseconomics/ethutils"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
"gopkg.in/leonelquinteros/gotext.v1"
)
// ValidateRecipient validates that the given input is valid.
//
// TODO: split up functino
func (h *MenuHandlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var AliasAddressResult string
var AliasAddress *models.AliasAddress
store := h.userdataStore
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
// remove white spaces
recipient := strings.ReplaceAll(string(input), " ", "")
if recipient == "0" {
return res, nil
}
recipientType, err := identity.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.Content = recipient
return res, nil
}
// save the recipient as the temporaryRecipient
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(recipient))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryRecipient entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", recipient, "error", err)
return res, err
}
switch recipientType {
case "phone number":
return h.handlePhoneNumber(ctx, sessionId, recipient, &res)
case "address":
return h.handleAddress(ctx, sessionId, recipient, &res)
case "alias":
return h.handleAlias(ctx, sessionId, recipient, &res)
}
return res, nil
}
func (h *MenuHandlers) handlePhoneNumber(ctx context.Context, sessionId, recipient string, res *resource.Result) (resource.Result, error) {
store := h.userdataStore
flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite")
formattedNumber, err := phone.FormatPhoneNumber(recipient)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to format phone number", "recipient", recipient, "error", err)
return *res, err
}
publicKey, err := store.ReadEntry(ctx, formattedNumber, storedb.DATA_PUBLIC_KEY)
if err != nil {
if db.IsNotFound(err) {
logg.InfoCtxf(ctx, "Unregistered phone number", "recipient", recipient)
res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite)
if recipient != "0" {
recipientType, err := identity.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.Content = recipient
return *res, nil
return res, nil
}
logg.ErrorCtxf(ctx, "Failed to read publicKey", "error", err)
return *res, err
}
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, publicKey); err != nil {
logg.ErrorCtxf(ctx, "Failed to write recipient", "value", string(publicKey), "error", err)
return *res, err
}
// Delegate to shared logic
if err := h.determineAndSaveTransactionType(ctx, sessionId, publicKey, []byte(formattedNumber)); err != nil {
return *res, err
}
return *res, nil
}
func (h *MenuHandlers) handleAddress(ctx context.Context, sessionId, recipient string, res *resource.Result) (resource.Result, error) {
store := h.userdataStore
address := ethutils.ChecksumAddress(recipient)
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(address)); err != nil {
logg.ErrorCtxf(ctx, "Failed to write recipient address", "error", err)
return *res, err
}
// normalize the address to fetch the recipient's phone number
publicKeyNormalized, err := hex.NormalizeHex(address)
if err != nil {
return *res, err
}
// get the recipient's phone number from the address
recipientPhoneNumber, err := store.ReadEntry(ctx, publicKeyNormalized, storedb.DATA_PUBLIC_KEY_REVERSE)
if err != nil || len(recipientPhoneNumber) == 0 {
logg.WarnCtxf(ctx, "Recipient address not registered, switching to normal transaction", "address", address)
recipientPhoneNumber = nil
}
if err := h.determineAndSaveTransactionType(ctx, sessionId, []byte(address), recipientPhoneNumber); err != nil {
return *res, err
}
return *res, nil
}
func (h *MenuHandlers) handleAlias(ctx context.Context, sessionId, recipient string, res *resource.Result) (resource.Result, error) {
store := h.userdataStore
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
var aliasAddressResult string
if strings.Contains(recipient, ".") {
alias, err := h.accountService.CheckAliasAddress(ctx, recipient)
if err == nil {
aliasAddressResult = alias.Address
} else {
logg.ErrorCtxf(ctx, "Failed to resolve alias", "alias", recipient, "error", err)
// save the recipient as the temporaryRecipient
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(recipient))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryRecipient entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", recipient, "error", err)
return res, err
}
} else {
for _, domain := range config.SearchDomains() {
fqdn := fmt.Sprintf("%s.%s", recipient, domain)
logg.InfoCtxf(ctx, "Trying alias", "fqdn", fqdn)
alias, err := h.accountService.CheckAliasAddress(ctx, fqdn)
if err == nil {
res.FlagReset = append(res.FlagReset, flag_api_call_error)
aliasAddressResult = alias.Address
break
switch recipientType {
case "phone number":
// format the phone number
formattedNumber, err := phone.FormatPhoneNumber(recipient)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to format the phone number: %s", recipient, "error", err)
return res, err
}
// Check if the phone number is registered
publicKey, err := store.ReadEntry(ctx, formattedNumber, storedb.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
}
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", storedb.DATA_PUBLIC_KEY, "error", err)
return res, err
}
// Save the publicKey as the recipient
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, publicKey)
if err != nil {
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", string(publicKey), "error", err)
return res, err
}
case "address":
// checksum the address
address := ethutils.ChecksumAddress(recipient)
// Save the valid Ethereum address as the recipient
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(address))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", recipient, "error", err)
return res, err
}
case "alias":
if strings.Contains(recipient, ".") {
AliasAddress, err = h.accountService.CheckAliasAddress(ctx, recipient)
if err == nil {
AliasAddressResult = AliasAddress.Address
} else {
logg.ErrorCtxf(ctx, "failed to resolve alias", "alias", recipient, "error_alias_check", err)
}
} else {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "Alias resolution failed", "alias", fqdn, "error", err)
return *res, nil
//Perform a search for each search domain,break on first match
for _, domain := range config.SearchDomains() {
fqdn := fmt.Sprintf("%s.%s", recipient, domain)
logg.InfoCtxf(ctx, "Resolving with fqdn alias", "alias", fqdn)
AliasAddress, err = h.accountService.CheckAliasAddress(ctx, fqdn)
if err == nil {
res.FlagReset = append(res.FlagReset, flag_api_error)
AliasAddressResult = AliasAddress.Address
continue
} else {
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed to resolve alias", "alias", recipient, "error_alias_check", err)
return res, nil
}
}
}
if AliasAddressResult == "" {
res.Content = recipient
res.FlagSet = append(res.FlagSet, flag_invalid_recipient)
return res, nil
} else {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(AliasAddressResult))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", AliasAddressResult, "error", err)
return res, err
}
}
}
}
if aliasAddressResult == "" {
res.FlagSet = append(res.FlagSet, flag_invalid_recipient)
res.Content = recipient
return *res, nil
}
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(aliasAddressResult)); err != nil {
logg.ErrorCtxf(ctx, "Failed to store alias recipient", "error", err)
return *res, err
}
// Normalize the alias address to fetch the recipient's phone number
publicKeyNormalized, err := hex.NormalizeHex(aliasAddressResult)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to normalize alias address", "address", aliasAddressResult, "error", err)
return *res, err
}
// get the recipient's phone number from the address
recipientPhoneNumber, err := store.ReadEntry(ctx, publicKeyNormalized, storedb.DATA_PUBLIC_KEY_REVERSE)
if err != nil || len(recipientPhoneNumber) == 0 {
logg.WarnCtxf(ctx, "Alias address not registered, switching to normal transaction", "address", aliasAddressResult)
recipientPhoneNumber = nil
}
if err := h.determineAndSaveTransactionType(ctx, sessionId, []byte(aliasAddressResult), recipientPhoneNumber); err != nil {
return *res, err
}
return *res, nil
}
// determineAndSaveTransactionType centralizes transaction-type logic and recipient info persistence.
// It expects the session to already have the recipient's public key (address) written.
func (h *MenuHandlers) determineAndSaveTransactionType(
ctx context.Context,
sessionId string,
publicKey []byte,
recipientPhoneNumber []byte,
) error {
store := h.userdataStore
txType := "swap"
// Read sender's active address
senderActiveAddress, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to read sender active address", "error", err)
return err
}
var recipientActiveAddress []byte
if recipientPhoneNumber != nil {
recipientActiveAddress, _ = store.ReadEntry(ctx, string(recipientPhoneNumber), storedb.DATA_ACTIVE_ADDRESS)
}
// recipient has no active token → normal transaction
if recipientActiveAddress == nil {
txType = "normal"
} else if senderActiveAddress != nil && string(senderActiveAddress) == string(recipientActiveAddress) {
// recipient has active token same as sender → normal transaction
txType = "normal"
}
// Save the transaction type
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte(txType)); err != nil {
logg.ErrorCtxf(ctx, "Failed to write transaction type", "type", txType, "error", err)
return err
}
// Save the recipient's phone number only if it exists
if recipientPhoneNumber != nil {
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER, recipientPhoneNumber); err != nil {
logg.ErrorCtxf(ctx, "Failed to write recipient phone number", "type", txType, "error", err)
return err
}
} else {
logg.InfoCtxf(ctx, "No recipient phone number found for public key", "publicKey", string(publicKey))
}
return nil
return res, nil
}
// TransactionReset resets the previous transaction data (Recipient and Amount)
@ -268,16 +162,6 @@ func (h *MenuHandlers) TransactionReset(ctx context.Context, sym string, input [
return res, nil
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte(""))
if err != nil {
return res, nil
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER, []byte(""))
if err != nil {
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_invalid_recipient, flag_invalid_recipient_with_invite)
return res, nil
@ -294,226 +178,40 @@ func (h *MenuHandlers) ResetTransactionAmount(ctx context.Context, sym string, i
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction")
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(""))
if err != nil {
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_invalid_amount, flag_swap_transaction)
res.FlagReset = append(res.FlagReset, flag_invalid_amount)
return res, nil
}
// MaxAmount checks the transaction type to determine the displayed max amount.
// If the transaction type is "swap", it checks the max swappable amount and sets this as the content.
// If the transaction type is "normal", gets the current sender's balance from the store and sets it as
// MaxAmount gets the current balance from the API and sets it as
// the result content.
func (h *MenuHandlers) MaxAmount(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")
}
store := h.userdataStore
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction")
userStore := h.userdataStore
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
// Fetch session data
transactionType, activeBal, activeSym, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId)
activeBal, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", storedb.DATA_ACTIVE_BAL, "error", err)
return res, err
}
// Format the active balance amount to 2 decimal places
formattedBalance, _ := store.TruncateDecimalString(string(activeBal), 2)
// If normal transaction, or if the sym is max_amount, return balance
if string(transactionType) == "normal" || sym == "max_amount" {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
res.Content = l.Get("Maximum amount: %s %s\nEnter amount:", formattedBalance, string(activeSym))
return res, nil
}
res.FlagSet = append(res.FlagSet, flag_swap_transaction)
// Get the recipient's phone number to read other data items
recipientPhoneNumber, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER)
if err != nil {
// invalid state
return res, err
}
recipientActiveSym, recipientActiveAddress, recipientActiveDecimal, err := h.getRecipientData(ctx, string(recipientPhoneNumber))
if err != nil {
return res, err
}
// Resolve active pool address
activePoolAddress, err := h.resolveActivePoolAddress(ctx, sessionId)
if err != nil {
return res, err
}
// Check if sender token is swappable
canSwap, err := h.accountService.CheckTokenInPool(ctx, string(activePoolAddress), string(activeAddress))
if err != nil || !canSwap.CanSwapFrom {
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on CheckTokenInPool", "error", err)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
res.Content = l.Get("Maximum amount: %s %s\nEnter amount:", formattedBalance, string(activeSym))
return res, nil
}
// retrieve the max credit send amounts
maxSAT, maxRAT, err := h.calculateSendCreditLimits(ctx, activePoolAddress, activeAddress, recipientActiveAddress, publicKey, activeDecimal, recipientActiveDecimal)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on calculateSendCreditLimits", "error", err)
return res, nil
}
// Fallback if below minimum
maxFloat, _ := strconv.ParseFloat(maxSAT, 64)
if maxFloat < 0.1 {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
res.Content = l.Get("Maximum amount: %s %s\nEnter amount:", formattedBalance, string(activeSym))
return res, nil
}
// Save max RAT amount to be used in validating the user's input
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, []byte(maxRAT))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write swap max amount (maxRAT)", "value", maxRAT, "error", err)
return res, err
}
// save swap related data for the swap preview
metadata := &dataserviceapi.TokenHoldings{
TokenAddress: string(recipientActiveAddress),
TokenSymbol: string(recipientActiveSym),
TokenDecimals: string(recipientActiveDecimal),
}
// Store the active swap_to data
if err := store.UpdateSwapToVoucherData(ctx, userStore, sessionId, metadata); err != nil {
logg.ErrorCtxf(ctx, "failed on UpdateSwapToVoucherData", "error", err)
return res, err
}
res.Content = l.Get(
"Credit Available: %s %s\n(You can swap up to %s %s -> %s %s).\nEnter %s amount:",
maxRAT,
string(recipientActiveSym),
maxSAT,
string(activeSym),
maxRAT,
string(recipientActiveSym),
string(recipientActiveSym),
)
res.Content = string(activeBal)
return res, nil
}
func (h *MenuHandlers) getSessionData(ctx context.Context, sessionId string) (transactionType, activeBal, activeSym, activeAddress, publicKey, activeDecimal []byte, err error) {
store := h.userdataStore
transactionType, err = store.ReadEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE)
if err != nil {
return
}
activeBal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL)
if err != nil {
return
}
activeAddress, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
if err != nil {
return
}
activeSym, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM)
if err != nil {
return
}
publicKey, err = store.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
return
}
activeDecimal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_DECIMAL)
if err != nil {
return
}
return
}
func (h *MenuHandlers) getRecipientData(ctx context.Context, sessionId string) (recipientActiveSym, recipientActiveAddress, recipientActiveDecimal []byte, err error) {
store := h.userdataStore
recipientActiveSym, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM)
if err != nil {
return
}
recipientActiveAddress, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
if err != nil {
return
}
recipientActiveDecimal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_DECIMAL)
if err != nil {
return
}
return
}
func (h *MenuHandlers) resolveActivePoolAddress(ctx context.Context, sessionId string) ([]byte, error) {
store := h.userdataStore
addr, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS)
if err == nil {
return addr, nil
}
if db.IsNotFound(err) {
defaultAddr := []byte(config.DefaultPoolAddress())
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS, defaultAddr); err != nil {
logg.ErrorCtxf(ctx, "failed to write default pool address", "error", err)
return nil, err
}
return defaultAddr, nil
}
logg.ErrorCtxf(ctx, "failed to read active pool address", "error", err)
return nil, err
}
func (h *MenuHandlers) calculateSendCreditLimits(ctx context.Context, poolAddress, fromAddress, toAddress, publicKey, fromDecimal, toDecimal []byte) (string, string, error) {
creditSendMaxLimits, err := h.accountService.GetCreditSendMaxLimit(
ctx,
string(poolAddress),
string(fromAddress),
string(toAddress),
string(publicKey),
)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetCreditSendMaxLimit", "error", err)
return "", "", err
}
scaledSAT := store.ScaleDownBalance(creditSendMaxLimits.MaxSAT, string(fromDecimal))
formattedSAT, _ := store.TruncateDecimalString(string(scaledSAT), 2)
scaledRAT := store.ScaleDownBalance(creditSendMaxLimits.MaxRAT, string(toDecimal))
formattedRAT, _ := store.TruncateDecimalString(string(scaledRAT), 2)
return formattedSAT, formattedRAT, nil
}
// ValidateAmount ensures that the given input is a valid amount and that
// it is not more than the current balance.
func (h *MenuHandlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
@ -585,7 +283,7 @@ func (h *MenuHandlers) GetRecipient(ctx context.Context, sym string, input []byt
recipient, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if len(recipient) == 0 {
logg.ErrorCtxf(ctx, "recipient is empty", "key", storedb.DATA_TEMPORARY_VALUE)
return res, fmt.Errorf("data error encountered")
return res, fmt.Errorf("Data error encountered")
}
res.Content = string(recipient)
@ -607,7 +305,7 @@ func (h *MenuHandlers) GetSender(ctx context.Context, sym string, input []byte)
return res, nil
}
// GetAmount retrieves the transaction amount from the store.
// GetAmount retrieves the amount from teh Gdbm Db.
func (h *MenuHandlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
@ -659,20 +357,9 @@ func (h *MenuHandlers) InitiateTransaction(ctx context.Context, sym string, inpu
// Call TokenTransfer
r, err := h.accountService.TokenTransfer(ctx, finalAmountStr, data.PublicKey, data.Recipient, data.ActiveAddress)
if err != nil {
var apiErr *http.APIError
if errors.As(err, &apiErr) {
switch apiErr.Code {
case "E10":
res.Content = l.Get("Only USD vouchers are allowed to mpesa.sarafu.eth.")
default:
res.Content = l.Get("Your request failed. Please try again later.")
}
} else {
res.Content = l.Get("An unexpected error occurred. Please try again later.")
}
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on TokenTransfer", "error", err)
return res, nil
}
@ -691,214 +378,3 @@ func (h *MenuHandlers) InitiateTransaction(ctx context.Context, sym string, inpu
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
// TransactionSwapPreview displays the send swap preview and estimates
func (h *MenuHandlers) TransactionSwapPreview(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")
}
// Input in RAT
inputStr := string(input)
if inputStr == "0" {
return res, nil
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
recipientPhoneNumber, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER)
if err != nil {
// invalid state
return res, err
}
swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId)
if err != nil {
return res, err
}
// use the stored max RAT
maxRATValue, err := strconv.ParseFloat(swapData.ActiveSwapMaxAmount, 64)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to convert the swapMaxAmount to a float", "error", err)
return res, err
}
inputAmount, err := strconv.ParseFloat(inputStr, 64)
if err != nil || inputAmount > maxRATValue {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
// Format the amount to 2 decimal places
formattedAmount, err := store.TruncateDecimalString(inputStr, 2)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
finalAmountStr, err := store.ParseAndScaleAmount(formattedAmount, swapData.ActiveSwapToDecimal)
if err != nil {
return res, err
}
// call the credit send API to get the reverse quote
r, err := h.accountService.GetCreditSendReverseQuote(ctx, swapData.ActivePoolAddress, swapData.ActiveSwapFromAddress, swapData.ActiveSwapToAddress, finalAmountStr)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed GetCreditSendReverseQuote poolSwap", "error", err)
return res, nil
}
sendInputAmount := r.InputAmount // amount of SAT that should be swapped
sendOutputAmount := r.OutputAmount // amount of RAT that will be received
// store the sendOutputAmount as the final amount (that will be sent)
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(sendOutputAmount))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write output amount value entry with", "key", storedb.DATA_AMOUNT, "value", sendOutputAmount, "error", err)
return res, err
}
// Scale down the quoted output amount
quoteAmountStr := store.ScaleDownBalance(sendOutputAmount, swapData.ActiveSwapToDecimal)
// Format the qouteAmount amount to 2 decimal places
qouteAmount, _ := store.TruncateDecimalString(quoteAmountStr, 2)
// store the qouteAmount in the temporary value
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(qouteAmount))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporary qouteAmount entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", qouteAmount, "error", err)
return res, err
}
// store the sendInputAmount as the swap amount
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_AMOUNT, []byte(sendInputAmount))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write swap amount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "value", sendInputAmount, "error", err)
return res, err
}
res.Content = l.Get(
"%s will receive %s %s",
string(recipientPhoneNumber), qouteAmount, swapData.ActiveSwapToSym,
)
return res, nil
}
// TransactionInitiateSwap calls the poolSwap and returns a confirmation based on the result.
func (h *MenuHandlers) TransactionInitiateSwap(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_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId)
if err != nil {
return res, err
}
swapAmount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_AMOUNT)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read swapAmount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "error", err)
return res, err
}
swapAmountStr := string(swapAmount)
// Call the poolSwap API
poolSwap, err := h.accountService.PoolSwap(ctx, swapAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil
}
swapTrackingId := poolSwap.TrackingId
logg.InfoCtxf(ctx, "poolSwap", "swapTrackingId", swapTrackingId)
// Initiate a send
recipientPublicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read swapAmount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "error", err)
return res, err
}
recipientPhoneNumber, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER)
if err != nil {
// invalid state
return res, err
}
// read the amount that should be sent
amount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_AMOUNT)
if err != nil {
// invalid state
return res, err
}
// Call TokenTransfer with the expected swap amount
tokenTransfer, err := h.accountService.TokenTransfer(ctx, string(amount), swapData.PublicKey, string(recipientPublicKey), swapData.ActiveSwapToAddress)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on TokenTransfer", "error", err)
return res, nil
}
trackingId := tokenTransfer.TrackingId
logg.InfoCtxf(ctx, "TokenTransfer", "trackingId", trackingId)
res.Content = l.Get(
"Your request has been sent. %s will receive %s %s from %s.",
string(recipientPhoneNumber),
swapData.TemporaryValue,
swapData.ActiveSwapToSym,
sessionId,
)
res.FlagReset = append(res.FlagReset, flag_account_authorized, flag_swap_transaction)
return res, nil
}
// ClearTransactionTypeFlag resets the flag when a user goes back.
func (h *MenuHandlers) ClearTransactionTypeFlag(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction")
inputStr := string(input)
if inputStr == "0" {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
return res, nil
}
return res, nil
}

View File

@ -20,7 +20,7 @@ func (h *MenuHandlers) CheckTransactions(ctx context.Context, sym string, input
}
flag_no_transfers, _ := h.flagManager.GetFlag("flag_no_transfers")
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_error")
userStore := h.userdataStore
logdb := h.logDb
@ -33,11 +33,11 @@ func (h *MenuHandlers) CheckTransactions(ctx context.Context, sym string, input
// Fetch transactions from the API using the public key
transactionsResp, err := h.accountService.FetchTransactions(ctx, string(publicKey))
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err)
return res, err
}
res.FlagReset = append(res.FlagReset, flag_api_call_error)
res.FlagReset = append(res.FlagReset, flag_api_error)
// Return if there are no transactions
if len(transactionsResp) == 0 {

View File

@ -289,11 +289,8 @@ func (h *MenuHandlers) GetVoucherDetails(ctx context.Context, sym string, input
}
res.FlagReset = append(res.FlagReset, flag_api_error)
// sanitize invalid characters
symbol := strings.ReplaceAll(voucherData.TokenSymbol, "USD₮", "USDT")
res.Content = fmt.Sprintf(
"Name: %s\nSymbol: %s\nProduct: %s\nLocation: %s", voucherData.TokenName, symbol, voucherData.TokenCommodity, voucherData.TokenLocation,
"Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", voucherData.TokenName, voucherData.TokenSymbol, voucherData.TokenCommodity, voucherData.TokenLocation,
)
return res, nil

View File

@ -272,7 +272,7 @@ func TestGetVoucherDetails(t *testing.T) {
TokenCommodity: "Farming",
}
expectedResult.Content = fmt.Sprintf(
"Name: %s\nSymbol: %s\nProduct: %s\nLocation: %s", tokenDetails.TokenName, tokenDetails.TokenSymbol, tokenDetails.TokenCommodity, tokenDetails.TokenLocation,
"Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", tokenDetails.TokenName, tokenDetails.TokenSymbol, tokenDetails.TokenCommodity, tokenDetails.TokenLocation,
)
mockAccountService.On("VoucherData", string(tokA_AAddress)).Return(tokenDetails, nil)
res, err := h.GetVoucherDetails(ctx, "SessionId", []byte(""))

View File

@ -86,7 +86,6 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService)
ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset)
ls.DbRs.AddLocalFunc("invite_valid_recipient", appHandlers.InviteValidRecipient)
ls.DbRs.AddLocalFunc("max_amount", appHandlers.MaxAmount)
ls.DbRs.AddLocalFunc("credit_max_amount", appHandlers.MaxAmount)
ls.DbRs.AddLocalFunc("validate_amount", appHandlers.ValidateAmount)
ls.DbRs.AddLocalFunc("reset_transaction_amount", appHandlers.ResetTransactionAmount)
ls.DbRs.AddLocalFunc("get_recipient", appHandlers.GetRecipient)
@ -137,10 +136,6 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService)
ls.DbRs.AddLocalFunc("swap_max_limit", appHandlers.SwapMaxLimit)
ls.DbRs.AddLocalFunc("swap_preview", appHandlers.SwapPreview)
ls.DbRs.AddLocalFunc("initiate_swap", appHandlers.InitiateSwap)
ls.DbRs.AddLocalFunc("transaction_swap_preview", appHandlers.TransactionSwapPreview)
ls.DbRs.AddLocalFunc("transaction_initiate_swap", appHandlers.TransactionInitiateSwap)
ls.DbRs.AddLocalFunc("clear_trans_type_flag", appHandlers.ClearTransactionTypeFlag)
ls.first = appHandlers.Init
return appHandlers, nil

View File

@ -5,19 +5,19 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"input": "2",
"expectedContent": "My vouchers\n1:Select voucher\n2:Voucher details\n0:Back"
},
{
"input": "1",
"expectedContent": "Select number or symbol from your vouchers:\n1:SRF\n0:Back\n99:Quit"
"expectedContent": "Select number or symbol from your vouchers:\n1SRF\n0:Back\n99:Quit"
},
{
"input": "",
"expectedContent": "Select number or symbol from your vouchers:\n1:SRF\n0:Back\n99:Quit"
"expectedContent": "Select number or symbol from your vouchers:\n1SRF\n0:Back\n99:Quit"
},
{
"input": "1",
@ -29,7 +29,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -38,15 +38,15 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"input": "2",
"expectedContent": "My vouchers\n1:Select voucher\n2:Voucher details\n0:Back"
},
{
"input": "1",
"expectedContent": "Select number or symbol from your vouchers:\n1:SRF\n0:Back\n99:Quit"
"expectedContent": "Select number or symbol from your vouchers:\n1SRF\n0:Back\n99:Quit"
},
{
"input": "SRF",
@ -58,7 +58,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -67,10 +67,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -95,7 +95,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -104,10 +104,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -136,7 +136,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -145,10 +145,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -177,7 +177,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -186,10 +186,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -210,7 +210,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -219,10 +219,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -235,7 +235,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -244,10 +244,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -280,7 +280,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -289,10 +289,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -325,7 +325,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -334,10 +334,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -382,7 +382,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -391,10 +391,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -419,7 +419,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -428,10 +428,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -460,7 +460,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -469,10 +469,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -497,7 +497,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -506,10 +506,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -534,7 +534,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -543,10 +543,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -571,7 +571,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -580,10 +580,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -604,7 +604,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@ -613,10 +613,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{

View File

@ -5,10 +5,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -49,7 +49,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}

View File

@ -5,10 +5,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -53,7 +53,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}

View File

@ -5,10 +5,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -45,7 +45,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}

View File

@ -5,10 +5,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -37,7 +37,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}

View File

@ -5,10 +5,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -33,7 +33,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}

View File

@ -5,10 +5,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -41,7 +41,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}

View File

@ -5,10 +5,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{
@ -61,7 +61,7 @@
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}

View File

@ -57,7 +57,7 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "1",
@ -86,10 +86,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "6",
"input": "4",
"expectedContent": "For more help,please call: 0757628885"
}
]
@ -99,7 +99,7 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "9",
@ -112,10 +112,10 @@
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "5",
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
},
{

View File

@ -1 +1,2 @@
{{.max_amount}}
Maximum amount: {{.max_amount}}
Enter amount:

View File

@ -1,6 +1,5 @@
LOAD reset_transaction_amount 10
RELOAD reset_transaction_amount
LOAD max_amount 0
LOAD reset_transaction_amount 0
LOAD max_amount 40
RELOAD max_amount
MAP max_amount
MOUT back 0
@ -10,7 +9,7 @@ RELOAD validate_amount
CATCH api_failure flag_api_call_error 1
CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0
LOAD get_recipient 100
LOAD get_recipient 0
LOAD get_sender 64
LOAD get_amount 32
INCMP transaction_pin *

View File

@ -1 +1,2 @@
{{.max_amount}}
Kiwango cha juu: {{.max_amount}}
Weka kiwango:

View File

@ -1 +0,0 @@
{{.credit_max_amount}}

View File

@ -1,19 +0,0 @@
LOAD reset_transaction_amount 10
LOAD credit_max_amount 160
RELOAD credit_max_amount
CATCH api_failure flag_api_call_error 1
MAP credit_max_amount
MOUT back 0
HALT
LOAD clear_trans_type_flag 6
RELOAD clear_trans_type_flag
CATCH transaction_swap flag_swap_transaction 1
LOAD validate_amount 64
RELOAD validate_amount
CATCH api_failure flag_api_call_error 1
CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0
LOAD get_recipient 0
LOAD get_sender 64
LOAD get_amount 32
INCMP transaction_pin *

View File

@ -1 +0,0 @@
{{.credit_max_amount}}

View File

@ -1 +0,0 @@
Enter recipient's phone number/address/alias:

View File

@ -1,12 +0,0 @@
LOAD transaction_reset 0
RELOAD transaction_reset
CATCH no_voucher flag_no_active_voucher 1
MOUT back 0
HALT
LOAD validate_recipient 50
RELOAD validate_recipient
CATCH api_failure flag_api_call_error 1
CATCH invalid_recipient flag_invalid_recipient 1
CATCH invite_recipient flag_invalid_recipient_with_invite 1
INCMP _ 0
INCMP credit_amount *

View File

@ -1 +0,0 @@
Credit-Send

View File

@ -1 +0,0 @@
Tuma-Mkopo

View File

@ -1 +0,0 @@
Weka nambari ya simu/anwani/lakabu:

View File

@ -1 +0,0 @@
Amount {{.transaction_swap_preview}} is invalid, please try again:

View File

@ -1,7 +0,0 @@
MAP transaction_swap_preview
RELOAD reset_transaction_amount
MOUT retry 1
MOUT quit 9
HALT
INCMP ^ 1
INCMP quit 9

View File

@ -1 +0,0 @@
Kiwango {{.transaction_swap_preview}} sio sahihi, tafadhali weka tena:

View File

@ -42,15 +42,3 @@ msgstr "%s haipo kwenye %s. Tafadhali badilisha sarafu yako na ujaribu tena."
msgid "Name: %s\nSymbol: %s"
msgstr "Jina: %s\nSarafu: %s"
msgid "Only USD vouchers are allowed to mpesa.sarafu.eth."
msgstr "Ni sarafu za USD pekee zinazoruhusiwa kwa mpesa.sarafu.eth."
msgid "Maximum amount: %s %s\nEnter amount:"
msgstr "Kiwango cha juu: %s %s\nWeka kiwango:"
msgid "Credit Available: %s %s\n(You can swap up to %s %s -> %s %s).\nEnter %s amount:"
msgstr "Kiwango kinachopatikana: %s %s\n(Unaweza kubadilisha hadi %s %s -> %s %s)\nWeka kiwango cha %s:"
msgid "%s will receive %s %s"
msgstr "%s atapokea %s %s"

View File

@ -7,20 +7,18 @@ LOAD check_balance 128
RELOAD check_balance
MAP check_balance
MOUT send 1
MOUT credit_send 2
MOUT swap 3
MOUT vouchers 4
MOUT select_pool 5
MOUT account 6
MOUT help 7
MOUT swap 2
MOUT vouchers 3
MOUT select_pool 4
MOUT account 5
MOUT help 6
MOUT quit 9
HALT
INCMP send 1
INCMP credit_send 2
INCMP swap_to_list 3
INCMP my_vouchers 4
INCMP select_pool 5
INCMP my_account 6
INCMP help 7
INCMP swap_to_list 2
INCMP my_vouchers 3
INCMP select_pool 4
INCMP my_account 5
INCMP help 6
INCMP quit 9
INCMP . *

View File

@ -35,4 +35,3 @@ flag,flag_account_pin_reset,41,this is set on an account when an admin triggers
flag,flag_incorrect_pool,42,this is set when the user selects an invalid pool
flag,flag_low_swap_amount,43,this is set when the swap max limit is less than 0.1
flag,flag_alias_unavailable,44,this is set when the preferred alias is not available
flag,flag_swap_transaction,45,this is set when the transaction will involve performing a swap

Can't render this file because it has a wrong number of fields in line 34.

View File

@ -1,6 +1,5 @@
CATCH no_voucher flag_no_active_voucher 1
LOAD get_pools 0
RELOAD get_pools
MAP get_pools
LOAD get_default_pool 20
RELOAD get_default_pool

View File

@ -1 +1 @@
Weka nambari ya simu/Anwani/Lakabu:
Weka nambari ya simu:

View File

@ -1 +0,0 @@
Badilisha

View File

@ -1,2 +1,2 @@
Chagua nambari au ishara ya sarafu unayotaka kupokea.
Chagua nambari au ishara ya sarafu kubadilisha KWENDA:
{{.swap_to_list}}

View File

@ -1,3 +0,0 @@
{{.transaction_swap_preview}}
Please enter your PIN to confirm:

View File

@ -1,13 +0,0 @@
LOAD transaction_swap_preview 0
MAP transaction_swap_preview
CATCH api_failure flag_api_call_error 1
CATCH invalid_credit_send_amount flag_invalid_amount 1
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 transaction_swap_initiated *

View File

@ -1,4 +0,0 @@
LOAD reset_incorrect_pin 6
CATCH _ flag_account_authorized 0
LOAD transaction_initiate_swap 0
HALT

View File

@ -1,3 +0,0 @@
{{.transaction_swap_preview}}
Tafadhali weka PIN yako kudhibitisha:

View File

@ -89,10 +89,6 @@ const (
DATA_ACTIVE_POOL_NAME
// Holds the active pool symbol for the swap
DATA_ACTIVE_POOL_SYM
// Holds the send transaction type
DATA_SEND_TRANSACTION_TYPE
// Holds the recipient formatted phone number
DATA_RECIPIENT_PHONE_NUMBER
)
const (

View File

@ -173,9 +173,9 @@ func UpdateSwapToVoucherData(ctx context.Context, store DataStore, sessionId str
logg.InfoCtxf(ctx, "UpdateSwapToVoucherData", "data", data)
// Active swap to voucher data entries
activeEntries := map[storedb.DataTyp][]byte{
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: []byte(data.TokenAddress),
storedb.DATA_ACTIVE_SWAP_TO_SYM: []byte(data.TokenSymbol),
storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: []byte(data.TokenDecimals),
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: []byte(data.TokenAddress),
}
// Write active data

View File

@ -14,13 +14,13 @@ func TestReadSwapData(t *testing.T) {
// Test swap data
swapData := map[storedb.DataTyp]string{
storedb.DATA_PUBLIC_KEY: publicKey,
storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e",
storedb.DATA_ACTIVE_SYM: "AMANI",
storedb.DATA_ACTIVE_DECIMAL: "6",
storedb.DATA_ACTIVE_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe",
storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD",
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
storedb.DATA_PUBLIC_KEY: publicKey,
storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e",
storedb.DATA_ACTIVE_SWAP_FROM_SYM: "AMANI",
storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: "6",
storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe",
storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD",
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
}
// Store the data
@ -53,16 +53,15 @@ func TestReadSwapPreviewData(t *testing.T) {
// Test swap preview data
swapPreviewData := map[storedb.DataTyp]string{
storedb.DATA_TEMPORARY_VALUE: "temp",
storedb.DATA_PUBLIC_KEY: publicKey,
storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT: "1339482",
storedb.DATA_ACTIVE_DECIMAL: "6",
storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e",
storedb.DATA_ACTIVE_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe",
storedb.DATA_ACTIVE_SYM: "AMANI",
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD",
storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: "18",
storedb.DATA_PUBLIC_KEY: publicKey,
storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT: "1339482",
storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: "6",
storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e",
storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe",
storedb.DATA_ACTIVE_SWAP_FROM_SYM: "AMANI",
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD",
storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: "18",
}
// Store the data
@ -73,7 +72,6 @@ func TestReadSwapPreviewData(t *testing.T) {
}
expectedResult := SwapPreviewData{
TemporaryValue: "temp",
PublicKey: "0X13242618721",
ActiveSwapMaxAmount: "1339482",
ActiveSwapFromDecimal: "6",

View File

@ -7,7 +7,6 @@ import (
"math/big"
"reflect"
"strconv"
"strings"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
)
@ -22,34 +21,25 @@ type TransactionData struct {
ActiveAddress string
}
// TruncateDecimalString safely truncates (not rounds) a number string to the specified decimal places
// TruncateDecimalString safely truncates the input amount to the specified decimal places
func TruncateDecimalString(input string, decimalPlaces int) (string, error) {
if _, err := strconv.ParseFloat(input, 64); err != nil {
num, ok := new(big.Float).SetString(input)
if !ok {
return "", fmt.Errorf("invalid input")
}
// Split input into integer and fractional parts
parts := strings.SplitN(input, ".", 2)
intPart := parts[0]
var fracPart string
// Multiply by 10^decimalPlaces
scale := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimalPlaces)), nil))
scaled := new(big.Float).Mul(num, scale)
if len(parts) == 2 {
fracPart = parts[1]
}
// Truncate by converting to int (chops off decimals)
intPart, _ := scaled.Int(nil)
// Truncate or pad fractional part
if len(fracPart) > decimalPlaces {
fracPart = fracPart[:decimalPlaces]
} else {
fracPart = fracPart + strings.Repeat("0", decimalPlaces-len(fracPart))
}
// Divide back to get truncated float
truncated := new(big.Float).Quo(new(big.Float).SetInt(intPart), scale)
// Handle zero decimal places
if decimalPlaces == 0 {
return intPart, nil
}
return fmt.Sprintf("%s.%s", intPart, fracPart), nil
// Format with fixed decimals
return truncated.Text('f', decimalPlaces), nil
}
func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) {

View File

@ -22,13 +22,6 @@ func TestTruncateDecimalString(t *testing.T) {
want: "4.00",
expectError: false,
},
{
name: "precision test",
input: "2.1",
decimalPlaces: 2,
want: "2.10",
expectError: false,
},
{
name: "single decimal",
input: "4.1",

View File

@ -15,11 +15,6 @@ var (
logg = logging.NewVanilla().WithDomain("vouchers").WithContextKey("SessionId")
)
// symbolReplacements holds mappings of invalid symbols → valid ones
var symbolReplacements = map[string]string{
"USD₮": "USDT",
}
// VoucherMetadata helps organize data fields
type VoucherMetadata struct {
Symbols string
@ -28,24 +23,13 @@ type VoucherMetadata struct {
Addresses string
}
// sanitizeSymbol replaces known invalid token symbols with normalized ones
func sanitizeSymbol(symbol string) string {
if replacement, ok := symbolReplacements[symbol]; ok {
return replacement
}
return symbol
}
// ProcessVouchers converts holdings into formatted strings
func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
var data VoucherMetadata
var symbols, balances, decimals, addresses []string
for i, h := range holdings {
// normalize token symbol before use
cleanSymbol := sanitizeSymbol(h.TokenSymbol)
symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, cleanSymbol))
symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol))
// Scale down the balance
scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals)

View File

@ -61,14 +61,13 @@ func TestProcessVouchers(t *testing.T) {
holdings := []dataserviceapi.TokenHoldings{
{TokenAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100000000"},
{TokenAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200000000"},
{TokenAddress: "0x41c143d63Qa", TokenSymbol: "USD₮", TokenDecimals: "6", Balance: "300000000"},
}
expectedResult := VoucherMetadata{
Symbols: "1:SRF\n2:MILO\n3:USDT",
Balances: "1:100\n2:20000\n3:300",
Decimals: "1:6\n2:4\n3:6",
Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa\n3:0x41c143d63Qa",
Symbols: "1:SRF\n2:MILO",
Balances: "1:100\n2:20000",
Decimals: "1:6\n2:4",
Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa",
}
result := ProcessVouchers(holdings)