diff --git a/go.mod b/go.mod index 6b8984b..17c2697 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.4 require ( git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66 git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20250417111317-2953f4c2f32e - git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63 + 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 diff --git a/go.sum b/go.sum index e59d51f..e76c54f 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,10 @@ git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251021120522- 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= diff --git a/handlers/application/balance_test.go b/handlers/application/balance_test.go index c8da2ea..6cf9d17 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/poolswap.go b/handlers/application/poolswap.go index a226d33..6f08978 100644 --- a/handlers/application/poolswap.go +++ b/handlers/application/poolswap.go @@ -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 @@ -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:", diff --git a/handlers/application/send.go b/handlers/application/send.go index ddddebd..70c126d 100644 --- a/handlers/application/send.go +++ b/handlers/application/send.go @@ -9,135 +9,239 @@ import ( "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" + 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_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 + } + + // 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) @@ -164,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 @@ -180,40 +294,223 @@ 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_error, _ := h.flagManager.GetFlag("flag_api_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_error) + logg.ErrorCtxf(ctx, "failed on CheckTokenInPool", "error", err) + } + 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_error) + return res, err + } + + // 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) { @@ -285,7 +582,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) @@ -307,7 +604,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 @@ -391,3 +688,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_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 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_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 + } + + // 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_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 +} + +// 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/local.go b/handlers/local.go index ed64020..0319a47 100644 --- a/handlers/local.go +++ b/handlers/local.go @@ -86,6 +86,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) @@ -136,6 +137,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..83094b6 100644 --- a/services/registration/amount.vis +++ b/services/registration/amount.vis @@ -1,5 +1,5 @@ -LOAD reset_transaction_amount 0 -LOAD max_amount 40 +LOAD reset_transaction_amount 10 +LOAD max_amount 160 RELOAD max_amount MAP max_amount MOUT back 0 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..f6b3055 --- /dev/null +++ b/services/registration/credit_amount.vis @@ -0,0 +1,18 @@ +LOAD reset_transaction_amount 10 +LOAD credit_max_amount 160 +RELOAD credit_max_amount +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 bf9f39a..96bfc07 100644 --- a/services/registration/locale/swa/default.po +++ b/services/registration/locale/swa/default.po @@ -45,3 +45,12 @@ 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/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/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 5ada4f6..4cc3f57 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",