diff --git a/go.mod b/go.mod index a380852..93677fc 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ toolchain go1.24.6 require ( 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.20250819094350-3c8e3d1bc86f - git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250819084006-5a9c82207578 - git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.9.0-beta.1.0.20250820130947-615f1d32acbe - github.com/alecthomas/assert/v2 v2.11.0 + git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028083421-fe897cca84f2 + 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 github.com/gofrs/uuid v4.4.0+incompatible github.com/grassrootseconomics/ethutils v1.3.1 github.com/grassrootseconomics/go-vise v0.5.0 diff --git a/go.sum b/go.sum index 24d1cea..fe85a39 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,36 @@ git.defalsify.org/vise.git v0.3.2-0.20250407143413-e55cf9bcb7d2 h1:kbiDZtvphEKsT git.defalsify.org/vise.git v0.3.2-0.20250407143413-e55cf9bcb7d2/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20250417111317-2953f4c2f32e h1:DcC9qkNl9ny3hxQmsMK6W81+5R/j4ZwYUbvewMI/rlc= git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20250417111317-2953f4c2f32e/go.mod h1:wgQJZGIS6QuNLHqDhcsvehsbn5PvgV7aziRebMnJi60= -git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250819094350-3c8e3d1bc86f h1:0cf7i1gNXZR9VqXjy45cKOB5lbnSzfZGkIvSQhQSF5I= -git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250819094350-3c8e3d1bc86f/go.mod h1:LJF/8GtEP/XU2+Z1KzN6//nFyqJbn17oledIn6Gtmmc= -git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250819084006-5a9c82207578 h1:GKhBMVbjGBus3eAp2tw3M66irOnEWWg0QEKVn0bBS8E= -git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250819084006-5a9c82207578/go.mod h1:hx6mjSyxKv5oxiJxB6EevJrMJIYjVoYxFEzBtpD+29c= -git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.9.0-beta.1.0.20250819143213-4e0d0b53e8e2 h1:ZAb4ENotTZOIR15TG6Cu9pyKxRYpAAVSBp49n2Py2ic= -git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.9.0-beta.1.0.20250819143213-4e0d0b53e8e2/go.mod h1:h9dhyoSJvgGHNqiFwyLANIEPswohNO5IRBrV+VvXbxI= -git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.9.0-beta.1.0.20250820130947-615f1d32acbe h1:l4Dw6/J269cBlJQlWMv5OnAMufeg+/qcGzJVkw1kocM= -git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.9.0-beta.1.0.20250820130947-615f1d32acbe/go.mod h1:h9dhyoSJvgGHNqiFwyLANIEPswohNO5IRBrV+VvXbxI= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250623063234-c1797e7a32b5 h1:VnRe01kHkZUBK/QjE7iV6gElSqSwQnAkWV3yCHtuYrI= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250623063234-c1797e7a32b5/go.mod h1:H97hR+VOnZvR5BiGVb0ScCPwH/IoKBOlKM+yrQNVpq0= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250623070026-d945964b0b46 h1:0+XkSRe7XSHa9WHXKpGPuC0myDszjchr4syH006lQ28= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250623070026-d945964b0b46/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250623075057-7b42d509e6d4 h1:W+8CC7x5eCPylkGy2TEoOpfJuiIlqzEzyYTzCLlY/u8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250623075057-7b42d509e6d4/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250624074830-5aa032400c12 h1:vD8biQmN36eouuE+TdxgXQjKisRf5cTGu/tMPv3afs0= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250624074830-5aa032400c12/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250624090744-339ba854c997 h1:8bCKyYoV4YiVBvCZlRclc3aQlBYpWhgtM35mvniDFD8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250624090744-339ba854c997/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250626065419-57ee409f9629 h1:ew3vCFrLS/7/8uULTTPCbsHzFntQ6X68SScnBEy3pl0= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250626065419-57ee409f9629/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630213135-50ee455e7069 h1:re+hdr5NAC6JqhyvjMCkgX17fFi0u3Mawc6RBnBJW8I= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630213135-50ee455e7069/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630213606-12940bb5f284 h1:2zMU9jPd6xEO6oY9oxr84sdT9G3d09eyAkjVBAz9eco= +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= +git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694/go.mod h1:DpibtYpnT3nG4Kn556hRAkdu4+CtiI/6MbnQHal51mQ= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= diff --git a/handlers/application/balance_test.go b/handlers/application/balance_test.go index f870111..5dcb64c 100644 --- a/handlers/application/balance_test.go +++ b/handlers/application/balance_test.go @@ -52,7 +52,7 @@ func TestCheckBalance(t *testing.T) { alias: "user72", activeSym: "SRF", activeBal: "10.967", - expectedResult: resource.Result{Content: "user72 balance: 10.96 SRF\n"}, + expectedResult: resource.Result{Content: "user72\nBalance: 10.96 SRF\n"}, expectError: false, }, } diff --git a/handlers/application/pools.go b/handlers/application/pools.go index 47e1199..3ead33e 100644 --- a/handlers/application/pools.go +++ b/handlers/application/pools.go @@ -22,12 +22,12 @@ func (h *MenuHandlers) GetPools(ctx context.Context, sym string, input []byte) ( } userStore := h.userdataStore - flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") + flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_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_error) + res.FlagSet = append(res.FlagSet, flag_api_call_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_error, _ := h.flagManager.GetFlag("flag_api_call_error") + flag_api_call_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_error) + res.FlagSet = append(res.FlagSet, flag_api_call_error) return res, nil } diff --git a/handlers/application/poolswap.go b/handlers/application/poolswap.go index 7d0c700..9fcf390 100644 --- a/handlers/application/poolswap.go +++ b/handlers/application/poolswap.go @@ -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_error, _ := h.flagManager.GetFlag("flag_api_error") + flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_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_error) + res.FlagSet = append(res.FlagSet, flag_api_call_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_error) + res.FlagSet = append(res.FlagSet, flag_api_call_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_error, _ := h.flagManager.GetFlag("flag_api_error") + flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_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_error) + res.FlagSet = append(res.FlagSet, flag_api_call_error) logg.ErrorCtxf(ctx, "failed on GetSwapFromTokenMaxLimit", "error", err) - return res, err + return res, nil } // 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 := fmt.Sprintf("%.2f", maxAmountFloat) + maxStr, _ := store.TruncateDecimalString(string(maxAmountStr), 2) 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_error, _ := h.flagManager.GetFlag("flag_api_call_error") - res.FlagSet = append(res.FlagSet, flag_api_error) + 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 @@ -319,14 +319,9 @@ 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 := fmt.Sprintf("%.2f", qouteAmount) + qouteStr, _ := store.TruncateDecimalString(string(quoteAmountStr), 2) res.Content = fmt.Sprintf( "You will swap:\n%s %s for %s %s:", @@ -369,8 +364,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_error, _ := h.flagManager.GetFlag("flag_api_call_error") - res.FlagSet = append(res.FlagSet, flag_api_error) + 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 diff --git a/handlers/application/send.go b/handlers/application/send.go index c96229b..4132dc8 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -2,140 +2,246 @@ 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/models" + "git.grassecon.net/grassrootseconomics/sarafu-api/remote/http" "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" - "github.com/grassrootseconomics/go-vise/db" - "github.com/grassrootseconomics/go-vise/resource" + 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 + } - 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) + 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) res.Content = recipient - - return res, nil + return *res, nil } + logg.ErrorCtxf(ctx, "Failed to read publicKey", "error", err) + return *res, 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 + 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) } + } else { + for _, domain := range config.SearchDomains() { + fqdn := fmt.Sprintf("%s.%s", recipient, domain) + logg.InfoCtxf(ctx, "Trying alias", "fqdn", fqdn) - 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) - } + alias, err := h.accountService.CheckAliasAddress(ctx, fqdn) + if err == nil { + res.FlagReset = append(res.FlagReset, flag_api_call_error) + aliasAddressResult = alias.Address + break } else { - //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 - } + res.FlagSet = append(res.FlagSet, flag_api_call_error) + logg.ErrorCtxf(ctx, "Alias resolution failed", "alias", fqdn, "error", err) + return *res, nil } } } - return res, nil + 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 } // TransactionReset resets the previous transaction data (Recipient and Amount) @@ -162,6 +268,16 @@ 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 @@ -178,40 +294,226 @@ 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) + res.FlagReset = append(res.FlagReset, flag_invalid_amount, flag_swap_transaction) return res, nil } -// MaxAmount gets the current balance from the API and sets it as +// 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 // 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 - activeBal, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL) + 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) if err != nil { - logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", storedb.DATA_ACTIVE_BAL, "error", err) return res, err } - res.Content = string(activeBal) + // 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), + ) 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) { @@ -283,7 +585,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) @@ -305,7 +607,7 @@ func (h *MenuHandlers) GetSender(ctx context.Context, sym string, input []byte) return res, nil } -// GetAmount retrieves the amount from teh Gdbm Db. +// GetAmount retrieves the transaction amount from the store. func (h *MenuHandlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result @@ -357,9 +659,20 @@ 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 { - 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.") + 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) logg.ErrorCtxf(ctx, "failed on TokenTransfer", "error", err) return res, nil } @@ -378,3 +691,214 @@ 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 +} diff --git a/handlers/application/transactions.go b/handlers/application/transactions.go index dd8e373..e39ebbc 100644 --- a/handlers/application/transactions.go +++ b/handlers/application/transactions.go @@ -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_error, _ := h.flagManager.GetFlag("flag_api_error") + flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error") userStore := h.userdataStore publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY) @@ -32,11 +32,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_error) + res.FlagSet = append(res.FlagSet, flag_api_call_error) logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) return res, err } - res.FlagReset = append(res.FlagReset, flag_api_error) + res.FlagReset = append(res.FlagReset, flag_api_call_error) // Return if there are no transactions if len(transactionsResp) == 0 { diff --git a/handlers/application/vouchers.go b/handlers/application/vouchers.go index b73b74e..38f6c9d 100644 --- a/handlers/application/vouchers.go +++ b/handlers/application/vouchers.go @@ -284,8 +284,11 @@ 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\nCommodity: %s\nLocation: %s", voucherData.TokenName, voucherData.TokenSymbol, voucherData.TokenCommodity, voucherData.TokenLocation, + "Name: %s\nSymbol: %s\nProduct: %s\nLocation: %s", voucherData.TokenName, symbol, voucherData.TokenCommodity, voucherData.TokenLocation, ) return res, nil diff --git a/handlers/application/vouchers_test.go b/handlers/application/vouchers_test.go index f1fa265..cd1b0eb 100644 --- a/handlers/application/vouchers_test.go +++ b/handlers/application/vouchers_test.go @@ -265,7 +265,7 @@ func TestGetVoucherDetails(t *testing.T) { TokenCommodity: "Farming", } expectedResult.Content = fmt.Sprintf( - "Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", tokenDetails.TokenName, tokenDetails.TokenSymbol, tokenDetails.TokenCommodity, tokenDetails.TokenLocation, + "Name: %s\nSymbol: %s\nProduct: %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("")) diff --git a/handlers/local.go b/handlers/local.go index 8e4a242..b7c0bd0 100644 --- a/handlers/local.go +++ b/handlers/local.go @@ -76,6 +76,7 @@ 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) @@ -126,6 +127,10 @@ 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 diff --git a/menutraversal_test/group_test.json b/menutraversal_test/group_test.json index 38c382e..587dc8a 100644 --- a/menutraversal_test/group_test.json +++ b/menutraversal_test/group_test.json @@ -5,19 +5,19 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "2", + "input": "3", "expectedContent": "My vouchers\n1:Select voucher\n2:Voucher details\n0:Back" }, { "input": "1", - "expectedContent": "Select number or symbol from your vouchers:\n1SRF\n0:Back\n99:Quit" + "expectedContent": "Select number or symbol from your vouchers:\n1:SRF\n0:Back\n99:Quit" }, { "input": "", - "expectedContent": "Select number or symbol from your vouchers:\n1SRF\n0:Back\n99:Quit" + "expectedContent": "Select number or symbol from your vouchers:\n1:SRF\n0:Back\n99:Quit" }, { "input": "1", @@ -29,7 +29,7 @@ }, { "input": "0", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -38,15 +38,15 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "2", + "input": "3", "expectedContent": "My vouchers\n1:Select voucher\n2:Voucher details\n0:Back" }, { "input": "1", - "expectedContent": "Select number or symbol from your vouchers:\n1SRF\n0:Back\n99:Quit" + "expectedContent": "Select number or symbol from your vouchers:\n1:SRF\n0:Back\n99:Quit" }, { "input": "SRF", @@ -58,7 +58,7 @@ }, { "input": "0", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -67,10 +67,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -104,10 +104,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -145,10 +145,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -186,10 +186,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -219,10 +219,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -244,10 +244,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -289,10 +289,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -334,10 +334,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -391,10 +391,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -428,10 +428,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -469,10 +469,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -506,10 +506,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -543,10 +543,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -580,10 +580,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] }, @@ -613,10 +613,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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" }, { diff --git a/menutraversal_test/profile_edit_start_familyname.json b/menutraversal_test/profile_edit_start_familyname.json index bb8bf0d..05e4d26 100644 --- a/menutraversal_test/profile_edit_start_familyname.json +++ b/menutraversal_test/profile_edit_start_familyname.json @@ -5,10 +5,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] } diff --git a/menutraversal_test/profile_edit_start_firstname.json b/menutraversal_test/profile_edit_start_firstname.json index 1fa2ca6..f3c4b3e 100644 --- a/menutraversal_test/profile_edit_start_firstname.json +++ b/menutraversal_test/profile_edit_start_firstname.json @@ -5,10 +5,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] } diff --git a/menutraversal_test/profile_edit_start_gender.json b/menutraversal_test/profile_edit_start_gender.json index d95420f..abfafc3 100644 --- a/menutraversal_test/profile_edit_start_gender.json +++ b/menutraversal_test/profile_edit_start_gender.json @@ -5,10 +5,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] } diff --git a/menutraversal_test/profile_edit_start_location.json b/menutraversal_test/profile_edit_start_location.json index 86541a3..184f9ab 100644 --- a/menutraversal_test/profile_edit_start_location.json +++ b/menutraversal_test/profile_edit_start_location.json @@ -5,10 +5,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] } diff --git a/menutraversal_test/profile_edit_start_offerings.json b/menutraversal_test/profile_edit_start_offerings.json index 2fc976a..b49b4d6 100644 --- a/menutraversal_test/profile_edit_start_offerings.json +++ b/menutraversal_test/profile_edit_start_offerings.json @@ -5,10 +5,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] } diff --git a/menutraversal_test/profile_edit_start_yob.json b/menutraversal_test/profile_edit_start_yob.json index 10c1d11..0b09357 100644 --- a/menutraversal_test/profile_edit_start_yob.json +++ b/menutraversal_test/profile_edit_start_yob.json @@ -5,10 +5,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] } diff --git a/menutraversal_test/profile_edit_when_adjacent_item_set.json b/menutraversal_test/profile_edit_when_adjacent_item_set.json index ec7c880..8df7c4d 100644 --- a/menutraversal_test/profile_edit_when_adjacent_item_set.json +++ b/menutraversal_test/profile_edit_when_adjacent_item_set.json @@ -5,10 +5,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" } ] } diff --git a/menutraversal_test/test_setup.json b/menutraversal_test/test_setup.json index 761760a..eb2f41d 100644 --- a/menutraversal_test/test_setup.json +++ b/menutraversal_test/test_setup.json @@ -57,7 +57,7 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { "input": "1", @@ -86,10 +86,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "4", + "input": "6", "expectedContent": "For more help,please call: 0757628885" } ] @@ -99,7 +99,7 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { "input": "9", @@ -112,10 +112,10 @@ "steps": [ { "input": "", - "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" + "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit" }, { - "input": "3", + "input": "5", "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" }, { diff --git a/services/registration/amount b/services/registration/amount index 9142aba..b50e87a 100644 --- a/services/registration/amount +++ b/services/registration/amount @@ -1,2 +1 @@ -Maximum amount: {{.max_amount}} -Enter amount: \ No newline at end of file +{{.max_amount}} \ No newline at end of file diff --git a/services/registration/amount.vis b/services/registration/amount.vis index c50691f..f8ff102 100644 --- a/services/registration/amount.vis +++ b/services/registration/amount.vis @@ -1,5 +1,6 @@ -LOAD reset_transaction_amount 0 -LOAD max_amount 40 +LOAD reset_transaction_amount 10 +RELOAD reset_transaction_amount +LOAD max_amount 0 RELOAD max_amount MAP max_amount MOUT back 0 @@ -9,7 +10,7 @@ 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_recipient 100 LOAD get_sender 64 LOAD get_amount 32 INCMP transaction_pin * diff --git a/services/registration/amount_swa b/services/registration/amount_swa index 0c8cf01..b50e87a 100644 --- a/services/registration/amount_swa +++ b/services/registration/amount_swa @@ -1,2 +1 @@ -Kiwango cha juu: {{.max_amount}} -Weka kiwango: \ No newline at end of file +{{.max_amount}} \ No newline at end of file diff --git a/services/registration/credit_amount b/services/registration/credit_amount new file mode 100644 index 0000000..5683878 --- /dev/null +++ b/services/registration/credit_amount @@ -0,0 +1 @@ +{{.credit_max_amount}} \ No newline at end of file diff --git a/services/registration/credit_amount.vis b/services/registration/credit_amount.vis new file mode 100644 index 0000000..f0f716f --- /dev/null +++ b/services/registration/credit_amount.vis @@ -0,0 +1,19 @@ +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 * diff --git a/services/registration/credit_amount_swa b/services/registration/credit_amount_swa new file mode 100644 index 0000000..5683878 --- /dev/null +++ b/services/registration/credit_amount_swa @@ -0,0 +1 @@ +{{.credit_max_amount}} \ No newline at end of file diff --git a/services/registration/credit_send b/services/registration/credit_send new file mode 100644 index 0000000..306466c --- /dev/null +++ b/services/registration/credit_send @@ -0,0 +1 @@ +Enter recipient's phone number/address/alias: \ No newline at end of file diff --git a/services/registration/credit_send.vis b/services/registration/credit_send.vis new file mode 100644 index 0000000..35db249 --- /dev/null +++ b/services/registration/credit_send.vis @@ -0,0 +1,12 @@ +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 * diff --git a/services/registration/credit_send_menu b/services/registration/credit_send_menu new file mode 100644 index 0000000..4362df7 --- /dev/null +++ b/services/registration/credit_send_menu @@ -0,0 +1 @@ +Credit-Send \ No newline at end of file diff --git a/services/registration/credit_send_menu_swa b/services/registration/credit_send_menu_swa new file mode 100644 index 0000000..2d8f3c2 --- /dev/null +++ b/services/registration/credit_send_menu_swa @@ -0,0 +1 @@ +Tuma-Mkopo \ No newline at end of file diff --git a/services/registration/credit_send_swa b/services/registration/credit_send_swa new file mode 100644 index 0000000..a44919b --- /dev/null +++ b/services/registration/credit_send_swa @@ -0,0 +1 @@ +Weka nambari ya simu/anwani/lakabu: \ No newline at end of file diff --git a/services/registration/invalid_credit_send_amount b/services/registration/invalid_credit_send_amount new file mode 100644 index 0000000..afe2d5d --- /dev/null +++ b/services/registration/invalid_credit_send_amount @@ -0,0 +1 @@ +Amount {{.transaction_swap_preview}} is invalid, please try again: \ No newline at end of file diff --git a/services/registration/invalid_credit_send_amount.vis b/services/registration/invalid_credit_send_amount.vis new file mode 100644 index 0000000..7870d57 --- /dev/null +++ b/services/registration/invalid_credit_send_amount.vis @@ -0,0 +1,7 @@ +MAP transaction_swap_preview +RELOAD reset_transaction_amount +MOUT retry 1 +MOUT quit 9 +HALT +INCMP ^ 1 +INCMP quit 9 diff --git a/services/registration/invalid_credit_send_amount_swa b/services/registration/invalid_credit_send_amount_swa new file mode 100644 index 0000000..4e0d8b8 --- /dev/null +++ b/services/registration/invalid_credit_send_amount_swa @@ -0,0 +1 @@ +Kiwango {{.transaction_swap_preview}} sio sahihi, tafadhali weka tena: \ No newline at end of file diff --git a/services/registration/locale/swa/default.po b/services/registration/locale/swa/default.po index 235624a..96bfc07 100644 --- a/services/registration/locale/swa/default.po +++ b/services/registration/locale/swa/default.po @@ -41,4 +41,16 @@ msgid "%s is not in %s. Please update your voucher and try again." msgstr "%s haipo kwenye %s. Tafadhali badilisha sarafu yako na ujaribu tena." msgid "Name: %s\nSymbol: %s" -msgstr "Jina: %s\nSarafu: %s" \ No newline at end of file +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" \ No newline at end of file diff --git a/services/registration/main.vis b/services/registration/main.vis index c48a43d..725aa74 100644 --- a/services/registration/main.vis +++ b/services/registration/main.vis @@ -7,18 +7,20 @@ LOAD check_balance 128 RELOAD check_balance MAP check_balance MOUT send 1 -MOUT swap 2 -MOUT vouchers 3 -MOUT select_pool 4 -MOUT account 5 -MOUT help 6 +MOUT credit_send 2 +MOUT swap 3 +MOUT vouchers 4 +MOUT select_pool 5 +MOUT account 6 +MOUT help 7 MOUT quit 9 HALT INCMP send 1 -INCMP swap_to_list 2 -INCMP my_vouchers 3 -INCMP select_pool 4 -INCMP my_account 5 -INCMP help 6 +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 quit 9 INCMP . * diff --git a/services/registration/pp.csv b/services/registration/pp.csv index 1e476c1..497f44b 100644 --- a/services/registration/pp.csv +++ b/services/registration/pp.csv @@ -35,3 +35,4 @@ 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 diff --git a/services/registration/select_pool.vis b/services/registration/select_pool.vis index 4436097..a193888 100644 --- a/services/registration/select_pool.vis +++ b/services/registration/select_pool.vis @@ -1,5 +1,6 @@ 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 diff --git a/services/registration/send_swa b/services/registration/send_swa index 016760e..5deea97 100644 --- a/services/registration/send_swa +++ b/services/registration/send_swa @@ -1 +1 @@ -Weka nambari ya simu: \ No newline at end of file +Weka nambari ya simu/Anwani/Lakabu: \ No newline at end of file diff --git a/services/registration/swap_menu_swa b/services/registration/swap_menu_swa new file mode 100644 index 0000000..b990e25 --- /dev/null +++ b/services/registration/swap_menu_swa @@ -0,0 +1 @@ +Badilisha \ No newline at end of file diff --git a/services/registration/swap_to_list_swa b/services/registration/swap_to_list_swa index 42da7d3..2dd923b 100644 --- a/services/registration/swap_to_list_swa +++ b/services/registration/swap_to_list_swa @@ -1,2 +1,2 @@ -Chagua nambari au ishara ya sarafu kubadilisha KWENDA: +Chagua nambari au ishara ya sarafu unayotaka kupokea. {{.swap_to_list}} \ No newline at end of file diff --git a/services/registration/transaction_swap b/services/registration/transaction_swap new file mode 100644 index 0000000..4120576 --- /dev/null +++ b/services/registration/transaction_swap @@ -0,0 +1,3 @@ +{{.transaction_swap_preview}} + +Please enter your PIN to confirm: \ No newline at end of file diff --git a/services/registration/transaction_swap.vis b/services/registration/transaction_swap.vis new file mode 100644 index 0000000..25e802c --- /dev/null +++ b/services/registration/transaction_swap.vis @@ -0,0 +1,13 @@ +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 * diff --git a/services/registration/transaction_swap_initiated.vis b/services/registration/transaction_swap_initiated.vis new file mode 100644 index 0000000..d6c0f57 --- /dev/null +++ b/services/registration/transaction_swap_initiated.vis @@ -0,0 +1,4 @@ +LOAD reset_incorrect_pin 6 +CATCH _ flag_account_authorized 0 +LOAD transaction_initiate_swap 0 +HALT diff --git a/services/registration/transaction_swap_swa b/services/registration/transaction_swap_swa new file mode 100644 index 0000000..9d2df58 --- /dev/null +++ b/services/registration/transaction_swap_swa @@ -0,0 +1,3 @@ +{{.transaction_swap_preview}} + +Tafadhali weka PIN yako kudhibitisha: \ No newline at end of file diff --git a/store/db/db.go b/store/db/db.go index 09cfdfa..9d4e53e 100644 --- a/store/db/db.go +++ b/store/db/db.go @@ -89,6 +89,10 @@ 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 ( diff --git a/store/swap.go b/store/swap.go index a63a33c..3510720 100644 --- a/store/swap.go +++ b/store/swap.go @@ -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 diff --git a/store/swap_test.go b/store/swap_test.go index ceabe99..3c0f0ee 100644 --- a/store/swap_test.go +++ b/store/swap_test.go @@ -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_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", + 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", } // Store the data @@ -53,15 +53,16 @@ func TestReadSwapPreviewData(t *testing.T) { // Test swap preview data swapPreviewData := map[storedb.DataTyp]string{ - 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", + 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", } // Store the data @@ -72,6 +73,7 @@ func TestReadSwapPreviewData(t *testing.T) { } expectedResult := SwapPreviewData{ + TemporaryValue: "temp", PublicKey: "0X13242618721", ActiveSwapMaxAmount: "1339482", ActiveSwapFromDecimal: "6", diff --git a/store/tokens.go b/store/tokens.go index 49ac175..0ac7bdf 100644 --- a/store/tokens.go +++ b/store/tokens.go @@ -7,6 +7,7 @@ import ( "math/big" "reflect" "strconv" + "strings" storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" ) @@ -21,25 +22,34 @@ type TransactionData struct { ActiveAddress string } -// TruncateDecimalString safely truncates the input amount to the specified decimal places +// TruncateDecimalString safely truncates (not rounds) a number string to the specified decimal places func TruncateDecimalString(input string, decimalPlaces int) (string, error) { - num, ok := new(big.Float).SetString(input) - if !ok { + if _, err := strconv.ParseFloat(input, 64); err != nil { return "", fmt.Errorf("invalid input") } - // 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) + // Split input into integer and fractional parts + parts := strings.SplitN(input, ".", 2) + intPart := parts[0] + var fracPart string - // Truncate by converting to int (chops off decimals) - intPart, _ := scaled.Int(nil) + if len(parts) == 2 { + fracPart = parts[1] + } - // Divide back to get truncated float - truncated := new(big.Float).Quo(new(big.Float).SetInt(intPart), scale) + // Truncate or pad fractional part + if len(fracPart) > decimalPlaces { + fracPart = fracPart[:decimalPlaces] + } else { + fracPart = fracPart + strings.Repeat("0", decimalPlaces-len(fracPart)) + } - // Format with fixed decimals - return truncated.Text('f', decimalPlaces), nil + // Handle zero decimal places + if decimalPlaces == 0 { + return intPart, nil + } + + return fmt.Sprintf("%s.%s", intPart, fracPart), nil } func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) { diff --git a/store/tokens_test.go b/store/tokens_test.go index 625a65d..34cca1e 100644 --- a/store/tokens_test.go +++ b/store/tokens_test.go @@ -22,6 +22,13 @@ 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", diff --git a/store/vouchers.go b/store/vouchers.go index e13e903..c419b5e 100644 --- a/store/vouchers.go +++ b/store/vouchers.go @@ -15,6 +15,11 @@ var ( logg = slogging.Get().With("component", "vouchers") ) +// 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 @@ -23,13 +28,24 @@ 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 { - symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol)) + // normalize token symbol before use + cleanSymbol := sanitizeSymbol(h.TokenSymbol) + + symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, cleanSymbol)) // Scale down the balance scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals) diff --git a/store/vouchers_test.go b/store/vouchers_test.go index 43f7d69..a458ecb 100644 --- a/store/vouchers_test.go +++ b/store/vouchers_test.go @@ -61,13 +61,14 @@ 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", - Balances: "1:100\n2:20000", - Decimals: "1:6\n2:4", - Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa", + 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", } result := ProcessVouchers(holdings)