From 4d687cac2e30511d265f71c8fdb252cc57b07304 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Wed, 30 Jul 2025 11:46:22 +0300 Subject: [PATCH 01/15] updated the MaxAmount description comment --- handlers/application/send.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/application/send.go b/handlers/application/send.go index 3580288..dad269e 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -189,7 +189,7 @@ func (h *MenuHandlers) ResetTransactionAmount(ctx context.Context, sym string, i return res, nil } -// MaxAmount gets the current balance from the API and sets it as +// MaxAmount 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 -- 2.45.2 From f441b3b2aff767065352a7f40b3bafccbef0f60e Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 4 Aug 2025 11:18:45 +0300 Subject: [PATCH 02/15] split the ValidateRecipient and check the transaction type --- handlers/application/send.go | 243 +++++++++++++++++++++-------------- 1 file changed, 146 insertions(+), 97 deletions(-) diff --git a/handlers/application/send.go b/handlers/application/send.go index dad269e..6170413 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -10,7 +10,6 @@ import ( "git.defalsify.org/vise.git/resource" "git.grassecon.net/grassrootseconomics/common/identity" "git.grassecon.net/grassrootseconomics/common/phone" - "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" @@ -19,123 +18,168 @@ import ( ) // 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 + } + + senderSym, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read sender activeSym", "error", err) + return *res, err + } + recipientActiveToken, err := store.ReadEntry(ctx, formattedNumber, storedb.DATA_ACTIVE_SYM) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read recipient activeSym", "error", err) + return *res, err + } + + txType := "swap" + if senderSym != nil && recipientActiveToken != nil && string(senderSym) == string(recipientActiveToken) { + txType = "normal" + } + if err := store.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte(txType)); err != nil { + logg.ErrorCtxf(ctx, "Failed to write tx type", "type", txType, "error", err) + return *res, err + } + if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_ACTIVE_TOKEN, recipientActiveToken); err != nil { + logg.ErrorCtxf(ctx, "Failed to write recipient active token", "error", err) + 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 + } + if err := store.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte("normal")); err != nil { + logg.ErrorCtxf(ctx, "Failed to write tx type for address", "error", err) + 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_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_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_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 + } + if err := store.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte("normal")); err != nil { + logg.ErrorCtxf(ctx, "Failed to write tx type for alias", "error", err) + return *res, err + } + + return *res, nil } // TransactionReset resets the previous transaction data (Recipient and Amount) @@ -162,6 +206,11 @@ 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 + } + res.FlagReset = append(res.FlagReset, flag_invalid_recipient, flag_invalid_recipient_with_invite) return res, nil @@ -283,7 +332,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) -- 2.45.2 From 758463ee8c3731547a33ab582d06e5a7e3e20905 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 4 Aug 2025 11:19:22 +0300 Subject: [PATCH 03/15] add transaction data keys --- store/db/db.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/store/db/db.go b/store/db/db.go index 5ada4f6..4b133bc 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 active token (RAT) + DATA_RECIPIENT_ACTIVE_TOKEN ) const ( -- 2.45.2 From b6de057cc48ec8fccb80eb7fcb0ecc85259a5499 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 4 Aug 2025 11:21:14 +0300 Subject: [PATCH 04/15] add a reset for the DATA_RECIPIENT_ACTIVE_TOKEN key --- handlers/application/send.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/handlers/application/send.go b/handlers/application/send.go index 6170413..5ccc58d 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -211,6 +211,11 @@ func (h *MenuHandlers) TransactionReset(ctx context.Context, sym string, input [ return res, nil } + err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_ACTIVE_TOKEN, []byte("")) + if err != nil { + return res, nil + } + res.FlagReset = append(res.FlagReset, flag_invalid_recipient, flag_invalid_recipient_with_invite) return res, nil -- 2.45.2 From 5b82afa768584fe8fab61a51f64dde40bb3477b4 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 4 Aug 2025 13:50:21 +0300 Subject: [PATCH 05/15] added the DATA_RECIPIENT_PHONE_NUMBER to store the formatted phone number --- store/db/db.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/store/db/db.go b/store/db/db.go index 4b133bc..c21a645 100644 --- a/store/db/db.go +++ b/store/db/db.go @@ -93,6 +93,8 @@ const ( DATA_SEND_TRANSACTION_TYPE // Holds the recipient active token (RAT) DATA_RECIPIENT_ACTIVE_TOKEN + // Holds the recipient formatted phone number + DATA_RECIPIENT_PHONE_NUMBER ) const ( -- 2.45.2 From e274967c8e767c7c468ddfd2c7a0a5fe919d1392 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 10:52:22 +0300 Subject: [PATCH 06/15] update the handlePhoneNumber logic to cover new users or those without an active voucher --- handlers/application/send.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/handlers/application/send.go b/handlers/application/send.go index 5ccc58d..805539a 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -94,23 +94,31 @@ func (h *MenuHandlers) handlePhoneNumber(ctx context.Context, sessionId, recipie logg.ErrorCtxf(ctx, "Failed to read sender activeSym", "error", err) return *res, err } - recipientActiveToken, err := store.ReadEntry(ctx, formattedNumber, storedb.DATA_ACTIVE_SYM) - if err != nil { - logg.ErrorCtxf(ctx, "Failed to read recipient activeSym", "error", err) - return *res, err - } + + recipientActiveToken, _ := store.ReadEntry(ctx, formattedNumber, storedb.DATA_ACTIVE_SYM) txType := "swap" - if senderSym != nil && recipientActiveToken != nil && string(senderSym) == string(recipientActiveToken) { + + // recipient has no active token → normal transaction + if recipientActiveToken == nil { + txType = "normal" + } else if senderSym != nil && string(senderSym) == string(recipientActiveToken) { + // recipient has active token same as sender → normal transaction txType = "normal" } + + // save transaction type if err := store.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte(txType)); err != nil { logg.ErrorCtxf(ctx, "Failed to write tx type", "type", txType, "error", err) return *res, err } - if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_ACTIVE_TOKEN, recipientActiveToken); err != nil { - logg.ErrorCtxf(ctx, "Failed to write recipient active token", "error", err) - return *res, err + + // only save recipient’s active token if it exists + if recipientActiveToken != nil { + if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_ACTIVE_TOKEN, recipientActiveToken); err != nil { + logg.ErrorCtxf(ctx, "Failed to write recipient active token", "error", err) + return *res, err + } } return *res, nil -- 2.45.2 From 7e1042c6a9fe775e4a4534dba875272df52191f9 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 12:46:16 +0300 Subject: [PATCH 07/15] update the MaxAmount logic to check the swap capability for swap transactions --- handlers/application/send.go | 142 +++++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 6 deletions(-) diff --git a/handlers/application/send.go b/handlers/application/send.go index 805539a..1bdae5d 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -251,29 +251,159 @@ func (h *MenuHandlers) ResetTransactionAmount(ctx context.Context, sym string, i return res, nil } -// MaxAmount gets the current sender's balance from the store 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_error, _ := h.flagManager.GetFlag("flag_api_error") + userStore := h.userdataStore + + // 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, return balance + if string(transactionType) == "normal" { + res.Content = fmt.Sprintf("%s %s", formattedBalance, string(activeSym)) + return res, nil + } + + // Get recipient token address + recipientTokenAddress, err := h.getRecipientTokenAddress(ctx, sessionId) + if err != nil { + // fallback to normal + res.Content = fmt.Sprintf("%s %s", formattedBalance, string(activeSym)) + return res, nil + } + + // 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_error) + logg.ErrorCtxf(ctx, "failed on CheckTokenInPool", "error", err) + } + res.Content = fmt.Sprintf("%s %s", formattedBalance, string(activeSym)) + return res, err + } + + // Calculate max swappable amount + maxStr, err := h.calculateSwapMaxAmount(ctx, activePoolAddress, activeAddress, recipientTokenAddress, publicKey, activeDecimal) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + return res, err + } + + // Fallback if below minimum + maxFloat, _ := strconv.ParseFloat(maxStr, 64) + if maxFloat < 0.1 { + res.Content = fmt.Sprintf("%s %s", formattedBalance, string(activeSym)) + return res, nil + } + + // Save max swap amount and return + err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, []byte(maxStr)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write swap max amount", "value", maxStr, "error", err) + return res, err + } + + res.Content = fmt.Sprintf("%s %s", maxStr, string(activeSym)) 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) + return +} + +func (h *MenuHandlers) getRecipientTokenAddress(ctx context.Context, sessionId string) ([]byte, error) { + store := h.userdataStore + recipientPhone, err := store.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER) + if err != nil { + return nil, err + } + return store.ReadEntry(ctx, string(recipientPhone), storedb.DATA_ACTIVE_ADDRESS) +} + +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) calculateSwapMaxAmount(ctx context.Context, poolAddress, fromAddress, toAddress, publicKey, decimal []byte) (string, error) { + swapLimit, err := h.accountService.GetSwapFromTokenMaxLimit( + ctx, + string(poolAddress), + string(fromAddress), + string(toAddress), + string(publicKey), + ) + if err != nil { + logg.ErrorCtxf(ctx, "failed on GetSwapFromTokenMaxLimit", "error", err) + return "", err + } + + scaled := store.ScaleDownBalance(swapLimit.Max, string(decimal)) + + formattedAmount, _ := store.TruncateDecimalString(string(scaled), 2) + return formattedAmount, 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) { -- 2.45.2 From 0e4dfe1baf803e39db24bdb4e6f0fc06617751bb Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 17:19:11 +0300 Subject: [PATCH 08/15] added the flag_swap_transaction when a swap needs to be performed in the send node --- services/registration/pp.csv | 1 + 1 file changed, 1 insertion(+) 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 -- 2.45.2 From 14d493475e8d69120af91b01704480107f6290d2 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 17:20:07 +0300 Subject: [PATCH 09/15] catch the flag_swap_transaction and move to the transaction_swap node --- services/registration/amount.vis | 1 + services/registration/transaction_swap | 3 +++ services/registration/transaction_swap.vis | 12 ++++++++++++ 3 files changed, 16 insertions(+) create mode 100644 services/registration/transaction_swap create mode 100644 services/registration/transaction_swap.vis diff --git a/services/registration/amount.vis b/services/registration/amount.vis index c50691f..d4786ab 100644 --- a/services/registration/amount.vis +++ b/services/registration/amount.vis @@ -4,6 +4,7 @@ RELOAD max_amount MAP max_amount MOUT back 0 HALT +CATCH transaction_swap flag_swap_transaction 1 LOAD validate_amount 64 RELOAD validate_amount CATCH api_failure flag_api_call_error 1 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..7f2f53e --- /dev/null +++ b/services/registration/transaction_swap.vis @@ -0,0 +1,12 @@ +LOAD transaction_swap_preview 0 +MAP transaction_swap_preview +CATCH api_failure flag_api_call_error 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 * -- 2.45.2 From 0c67efedea4f2577fa24a446c0acdc8fc59d16a0 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 17:20:34 +0300 Subject: [PATCH 10/15] added the transaction_swap_initiated node --- services/registration/transaction_swap_initiated.vis | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 services/registration/transaction_swap_initiated.vis 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 -- 2.45.2 From 5a09d33be0dc2fd0230640813a3f9f7bce71d7a3 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 17:21:23 +0300 Subject: [PATCH 11/15] removed unused DATA_RECIPIENT_ACTIVE_TOKEN key --- store/db/db.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/store/db/db.go b/store/db/db.go index c21a645..4cc3f57 100644 --- a/store/db/db.go +++ b/store/db/db.go @@ -91,8 +91,6 @@ const ( DATA_ACTIVE_POOL_SYM // Holds the send transaction type DATA_SEND_TRANSACTION_TYPE - // Holds the recipient active token (RAT) - DATA_RECIPIENT_ACTIVE_TOKEN // Holds the recipient formatted phone number DATA_RECIPIENT_PHONE_NUMBER ) -- 2.45.2 From 0f8c2f9270d47b3ac811eae744fcffaea97a2c2b Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 17:24:58 +0300 Subject: [PATCH 12/15] use the recipient's phone number to read swap related data --- handlers/application/send.go | 76 +++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/handlers/application/send.go b/handlers/application/send.go index 1bdae5d..f555a0c 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -14,6 +14,7 @@ import ( "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" ) @@ -89,20 +90,20 @@ func (h *MenuHandlers) handlePhoneNumber(ctx context.Context, sessionId, recipie return *res, err } - senderSym, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM) + senderActiveAddress, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS) if err != nil { - logg.ErrorCtxf(ctx, "Failed to read sender activeSym", "error", err) + logg.ErrorCtxf(ctx, "Failed to read sender senderActiveAddress", "error", err) return *res, err } - recipientActiveToken, _ := store.ReadEntry(ctx, formattedNumber, storedb.DATA_ACTIVE_SYM) + recipientActiveAddress, _ := store.ReadEntry(ctx, formattedNumber, storedb.DATA_ACTIVE_ADDRESS) txType := "swap" // recipient has no active token → normal transaction - if recipientActiveToken == nil { + if recipientActiveAddress == nil { txType = "normal" - } else if senderSym != nil && string(senderSym) == string(recipientActiveToken) { + } else if senderActiveAddress != nil && string(senderActiveAddress) == string(recipientActiveAddress) { // recipient has active token same as sender → normal transaction txType = "normal" } @@ -113,12 +114,10 @@ func (h *MenuHandlers) handlePhoneNumber(ctx context.Context, sessionId, recipie return *res, err } - // only save recipient’s active token if it exists - if recipientActiveToken != nil { - if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_ACTIVE_TOKEN, recipientActiveToken); err != nil { - logg.ErrorCtxf(ctx, "Failed to write recipient active token", "error", err) - return *res, err - } + // save the recipient's phone number + if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER, []byte(formattedNumber)); err != nil { + logg.ErrorCtxf(ctx, "Failed to write recipient's phone number", "type", txType, "error", err) + return *res, err } return *res, nil @@ -219,7 +218,7 @@ func (h *MenuHandlers) TransactionReset(ctx context.Context, sym string, input [ return res, nil } - err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_ACTIVE_TOKEN, []byte("")) + err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER, []byte("")) if err != nil { return res, nil } @@ -264,6 +263,7 @@ func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte) } flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") + flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction") userStore := h.userdataStore // Fetch session data @@ -277,16 +277,22 @@ func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte) // If normal transaction, return balance if string(transactionType) == "normal" { + res.FlagReset = append(res.FlagReset, flag_swap_transaction) res.Content = fmt.Sprintf("%s %s", formattedBalance, string(activeSym)) return res, nil } - // Get recipient token address - recipientTokenAddress, err := h.getRecipientTokenAddress(ctx, sessionId) + 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 { - // fallback to normal - res.Content = fmt.Sprintf("%s %s", formattedBalance, string(activeSym)) - return res, 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 @@ -307,7 +313,7 @@ func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte) } // Calculate max swappable amount - maxStr, err := h.calculateSwapMaxAmount(ctx, activePoolAddress, activeAddress, recipientTokenAddress, publicKey, activeDecimal) + maxStr, err := h.calculateSwapMaxAmount(ctx, activePoolAddress, activeAddress, recipientActiveAddress, publicKey, activeDecimal) if err != nil { res.FlagSet = append(res.FlagSet, flag_api_error) return res, err @@ -327,6 +333,20 @@ func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte) return res, err } + // save swap related data for the swap preview + metadata := &dataserviceapi.TokenHoldings{ + TokenSymbol: string(recipientActiveSym), + Balance: formattedBalance, //not used + TokenDecimals: string(recipientActiveDecimal), + TokenAddress: string(recipientActiveAddress), + } + + // 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 = fmt.Sprintf("%s %s", maxStr, string(activeSym)) return res, nil } @@ -355,16 +375,28 @@ func (h *MenuHandlers) getSessionData(ctx context.Context, sessionId string) (tr return } activeDecimal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_DECIMAL) + if err != nil { + return + } return } -func (h *MenuHandlers) getRecipientTokenAddress(ctx context.Context, sessionId string) ([]byte, error) { +func (h *MenuHandlers) getRecipientData(ctx context.Context, sessionId string) (recipientActiveSym, recipientActiveAddress, recipientActiveDecimal []byte, err error) { store := h.userdataStore - recipientPhone, err := store.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER) + + recipientActiveSym, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM) if err != nil { - return nil, err + return } - return store.ReadEntry(ctx, string(recipientPhone), storedb.DATA_ACTIVE_ADDRESS) + 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) { -- 2.45.2 From 4492f8087a4b2e9a70293677b263235a132cc19c Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 17:25:59 +0300 Subject: [PATCH 13/15] added TransactionSwapPreview functionality --- handlers/application/send.go | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/handlers/application/send.go b/handlers/application/send.go index f555a0c..38ef181 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -602,3 +602,104 @@ 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") + } + + 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 + } + + maxValue, 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 > maxValue { + 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.ActiveSwapFromDecimal) + if err != nil { + return res, err + } + + err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_AMOUNT, []byte(finalAmountStr)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write swap amount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "value", finalAmountStr, "error", err) + return res, err + } + // store the user's input amount in the temporary value + err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(inputStr)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write swap amount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "value", finalAmountStr, "error", err) + return res, err + } + + // 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) + res.Content = l.Get("Your request failed. Please try again later.") + logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err) + return res, nil + } + + // Scale down the quoted amount + quoteAmountStr := store.ScaleDownBalance(r.OutValue, swapData.ActiveSwapToDecimal) + + fmt.Println("the quoteAmountStr is:", quoteAmountStr) + 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) + + res.Content = fmt.Sprintf( + "%s will receive %s %s", + string(recipientPhoneNumber), qouteStr, swapData.ActiveSwapToSym, + ) + + return res, nil +} -- 2.45.2 From cda2d49f3e52071c4becfc6911d61d1486716376 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 17:27:20 +0300 Subject: [PATCH 14/15] added TransactionInitiateSwap functionality that performs a swap followed by a send --- handlers/application/send.go | 81 ++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/handlers/application/send.go b/handlers/application/send.go index 38ef181..49963af 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -703,3 +703,84 @@ func (h *MenuHandlers) TransactionSwapPreview(ctx context.Context, sym string, i 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_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 + } + + 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 + } + + // Call TokenTransfer + tokenTransfer, err := h.accountService.TokenTransfer(ctx, swapAmountStr, swapData.PublicKey, string(recipientPublicKey), swapData.ActiveSwapToAddress) + 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.") + 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 +} -- 2.45.2 From c90d3cd73104adcce9bf97ff9fd4a454ec553939 Mon Sep 17 00:00:00 2001 From: Alfred Kamanda Date: Mon, 25 Aug 2025 17:28:11 +0300 Subject: [PATCH 15/15] added the handler functions --- handlers/local.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/handlers/local.go b/handlers/local.go index ed64020..4742041 100644 --- a/handlers/local.go +++ b/handlers/local.go @@ -136,6 +136,9 @@ 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.first = appHandlers.Init return appHandlers, nil -- 2.45.2