Compare commits

..

42 Commits

Author SHA1 Message Date
7bf71cbfff Merge pull request 'allow-stables-direct-to-mpesa' (#118) from allow-stables-direct-to-mpesa into master
Reviewed-on: #118
2026-02-26 18:17:42 +01:00
295ca6e53e
use the token transfer API for pool deposit
Some checks failed
release / docker (push) Has been cancelled
2026-02-26 14:24:04 +03:00
de32deab80
set the flag_low_swap_amount for amounts below the min withdrawal
Some checks failed
release / docker (push) Has been cancelled
2026-02-25 15:51:48 +03:00
1a9dd64dd6
ensure that any stable coin can be sent to Mpesa without a swap
Some checks failed
release / docker (push) Has been cancelled
2026-02-25 14:40:16 +03:00
0d92872d90
ensure that any stable coin can do a direct transfer to Mpesa 2026-02-25 13:35:48 +03:00
77f0585b56
add the USDC token address as a stable voucher 2026-02-25 13:34:33 +03:00
38f0058d0a Merge pull request 'debt-menu' (#115) from debt-menu into master
Reviewed-on: #115
2026-02-25 09:44:57 +01:00
c16c39f289
update the translations for the swahili menus 2026-02-25 11:37:36 +03:00
185ff0dc45
have a single view for the pay_debt node 2026-02-25 11:35:58 +03:00
45ccefe1fe
properly format the comments 2026-02-25 10:16:29 +03:00
eea51ea40d
reset appropriate error flags on success
Some checks failed
release / docker (push) Has been cancelled
2026-02-23 17:46:52 +03:00
43c4b64b42
correctly calculate the credit
Some checks failed
release / docker (push) Has been cancelled
2026-02-23 16:20:36 +03:00
686f119a9e
remove unused CATCH statement 2026-02-23 16:16:15 +03:00
759e424805
use the correct sym for proper error handling 2026-02-23 16:15:59 +03:00
a270079008
add a fix for when users input a value when no vouchers exist 2026-02-23 16:15:12 +03:00
62eb132b32
add a translation for the pay debt menu 2026-02-23 16:14:31 +03:00
f198ecd913
have different syms to customize the final output when one has a single voucher 2026-02-23 16:11:19 +03:00
6e426bf6a0
return a default credit and debt response when one doesn't have a voucher 2026-02-23 11:05:43 +03:00
3bfa9820dd
remove debug statements
Some checks failed
release / docker (push) Has been cancelled
2026-02-20 13:35:28 +03:00
a2e2c0d68e
ensure the number is valid 2026-02-20 13:33:43 +03:00
e5b9a8955d
include the retrieved phone number 2026-02-20 13:30:21 +03:00
301d4f4232
add debug statements 2026-02-20 13:28:29 +03:00
9e93bb4b59
revert to a normal transaction of the recipient phone number is not present 2026-02-20 13:25:39 +03:00
6553c5a773
added error logs and read data keys directly 2026-02-20 13:16:23 +03:00
f948f7f27e
remove the amount multiplication by 1.015
Some checks failed
release / docker (push) Has been cancelled
2026-02-20 09:54:21 +03:00
9646cc2955
add a CATCH when one cannot swap from the current pool 2026-02-20 09:48:10 +03:00
bfef77e20e
add a CATCH for low amounts and API errors 2026-02-20 09:41:03 +03:00
3108cb2f22
set a default content of 0 if rates are not found for the selected voucher 2026-02-20 09:34:58 +03:00
320d10890c
improve the error message when one has a low swap amount 2026-02-19 20:09:24 +03:00
3595ff0d61
include the active symbol in the displayed limit 2026-02-19 20:08:57 +03:00
29cc4c63eb
correctly CATCH error flags 2026-02-19 20:02:44 +03:00
0280211197
set a default value of 0 on an API error 2026-02-19 20:01:58 +03:00
2e48fbad00
update the vis files to CATCH the low amount flag 2026-02-19 19:51:50 +03:00
cdd83dfd73
update the credit and debt calculations 2026-02-19 19:36:23 +03:00
0ef706a47e
include the word pool and update the translation
Some checks failed
release / docker (push) Has been cancelled
2026-02-19 09:15:09 +03:00
dec8fbc3f0
use the pool symbol in place of the pool name 2026-02-19 09:04:52 +03:00
29863d385d
remove unused OutputAmount 2026-02-19 08:43:03 +03:00
da8c8c711f
Merge branch 'master' into debt-menu 2026-02-19 08:40:51 +03:00
0d76b970d2 Merge pull request 'credit-send-pool-selection-hotfix' (#117) from credit-send-pool-selection-hotfix into master
Reviewed-on: #117
2026-02-19 06:31:21 +01:00
f32f93dff2
retain the original amount and use it for the transfer once the swap is performed 2026-02-18 13:15:41 +03:00
5ee99cdcd8
convert the user input to uppercase before calling the pool Details API 2026-02-18 12:42:25 +03:00
6abcb97f3a
multiply the final amount by 1.015 to slightly increase the swapped amount 2026-02-18 12:27:22 +03:00
31 changed files with 248 additions and 197 deletions

View File

@ -39,7 +39,7 @@ DEFAULT_MPESA_ASSET=cUSD
MPESA_BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr MPESA_BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
MPESA_ONRAMP_BASE=https://pretium.v1.grassecon.net MPESA_ONRAMP_BASE=https://pretium.v1.grassecon.net
# Known stable voucher addresses (USDm, USD₮) # Known stable voucher addresses (USDm, USD₮, USDC)
STABLE_VOUCHER_ADDRESSES=0x765DE816845861e75A25fCA122bb6898B8B1282a,0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e STABLE_VOUCHER_ADDRESSES=0x765DE816845861e75A25fCA122bb6898B8B1282a,0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e,0xcebA9300f2b948710d2653dD7B07f33A8B32118C
DEFAULT_STABLE_VOUCHER_ADDRESS=0x765DE816845861e75A25fCA122bb6898B8B1282a DEFAULT_STABLE_VOUCHER_ADDRESS=0x765DE816845861e75A25fCA122bb6898B8B1282a
DEFAULT_STABLE_VOUCHER_DECIMALS=18 DEFAULT_STABLE_VOUCHER_DECIMALS=18

View File

@ -3,12 +3,10 @@ package application
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"strconv" "strconv"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/sarafu-vise/config"
"git.grassecon.net/grassrootseconomics/sarafu-vise/store" "git.grassecon.net/grassrootseconomics/sarafu-vise/store"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
@ -114,8 +112,6 @@ func (h *MenuHandlers) CalculateCreditAndDebt(ctx context.Context, sym string, i
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
userStore := h.userdataStore
code := codeFromCtx(ctx) code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code) l := gotext.NewLocale(translationDir, code)
l.AddDomain("default") l.AddDomain("default")
@ -125,13 +121,11 @@ func (h *MenuHandlers) CalculateCreditAndDebt(ctx context.Context, sym string, i
// Fetch session data // Fetch session data
_, activeBal, activeSym, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId) _, activeBal, activeSym, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId)
if err != nil { if err != nil {
res.Content = l.Get("Credit: %s KSH\nDebt: %s %s\n", "0", "0", string(activeSym))
return res, nil return res, nil
} }
// default response
formattedDebt, _ := store.TruncateDecimalString(string(activeBal), 2)
res.FlagReset = append(res.FlagReset, flag_api_call_error) res.FlagReset = append(res.FlagReset, flag_api_call_error)
res.Content = l.Get("Credit: %s KSH\nDebt: %s %s\n", "0", formattedDebt, string(activeSym))
// Resolve active pool // Resolve active pool
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId) activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
@ -139,106 +133,93 @@ func (h *MenuHandlers) CalculateCreditAndDebt(ctx context.Context, sym string, i
return res, err return res, err
} }
// Fetch swappable vouchers // Fetch swappable vouchers (pool view)
swappableVouchers, err := h.accountService.GetPoolSwappableFromVouchers(ctx, string(activePoolAddress), string(publicKey)) swappableVouchers, err := h.accountService.GetPoolSwappableFromVouchers(ctx, string(activePoolAddress), string(publicKey))
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed on GetPoolSwappableFromVouchers", "error", err) logg.ErrorCtxf(ctx, "failed on GetPoolSwappableFromVouchers", "error", err)
res.Content = l.Get("Credit: %s KSH\nDebt: %s %s\n", "0", "0", string(activeSym))
return res, nil return res, nil
} }
if len(swappableVouchers) == 0 { if len(swappableVouchers) == 0 {
res.Content = l.Get("Credit: %s KSH\nDebt: %s %s\n", "0", "0", string(activeSym))
return res, nil return res, nil
} }
// Build stable voucher priority (lower index = higher priority) // Fetch ALL wallet vouchers (voucher holdings view)
stablePriority := make(map[string]int) allVouchers, err := h.accountService.FetchVouchers(ctx, string(publicKey))
stableAddresses := config.StableVoucherAddresses() if err != nil {
for i, addr := range stableAddresses { logg.ErrorCtxf(ctx, "failed on FetchVouchers", "error", err)
stablePriority[addr] = i return res, nil
} }
stable := make([]dataserviceapi.TokenHoldings, 0) // CREDIT calculation
nonStable := make([]dataserviceapi.TokenHoldings, 0) // Rule:
// 1. Swap quote of active voucher → first stable in pool from GetPoolSwappableFromVouchers
// 2. PLUS all stable balances from FetchVouchers
// Helper: order vouchers (stable first, priority-based)
orderVouchers := func(vouchers []dataserviceapi.TokenHoldings) []dataserviceapi.TokenHoldings {
for _, v := range vouchers {
if isStableVoucher(v.TokenAddress) {
stable = append(stable, v)
} else {
nonStable = append(nonStable, v)
}
}
sort.SliceStable(stable, func(i, j int) bool {
ai := stablePriority[stable[i].TokenAddress]
aj := stablePriority[stable[j].TokenAddress]
return ai < aj
})
return append(stable, nonStable...)
}
// Remove active voucher
filteredVouchers := make([]dataserviceapi.TokenHoldings, 0, len(swappableVouchers))
for _, v := range swappableVouchers {
if v.TokenSymbol != string(activeSym) {
filteredVouchers = append(filteredVouchers, v)
}
}
// Order remaining vouchers
orderedFilteredVouchers := orderVouchers(filteredVouchers)
// Process & store
data := store.ProcessVouchers(orderedFilteredVouchers)
dataMap := map[storedb.DataTyp]string{
storedb.DATA_VOUCHER_SYMBOLS: data.Symbols,
storedb.DATA_VOUCHER_BALANCES: data.Balances,
storedb.DATA_VOUCHER_DECIMALS: data.Decimals,
storedb.DATA_VOUCHER_ADDRESSES: data.Addresses,
}
for key, value := range dataMap {
if err := userStore.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil {
logg.ErrorCtxf(ctx, "Failed to write data entry for sessionId: %s", sessionId, "key", key, "error", err)
continue
}
}
// Credit calculation: How much Active Token that can be swapped for a stable coin
// + any stables sendable to Pretium (in KSH value)
scaledCredit := "0" scaledCredit := "0"
finalAmountStr, err := store.ParseAndScaleAmount(string(activeBal), string(activeDecimal)) // 1. Find first stable voucher in POOL (for swap target)
var firstPoolStable *dataserviceapi.TokenHoldings
for i := range swappableVouchers {
if isStableVoucher(swappableVouchers[i].TokenAddress) {
firstPoolStable = &swappableVouchers[i]
break
}
}
// 2. If pool has a stable, get swap quote
if firstPoolStable != nil {
finalAmountStr, err := store.ParseAndScaleAmount(
string(activeBal),
string(activeDecimal),
)
if err != nil { if err != nil {
return res, err return res, err
} }
// do a swap quote for default stable coin from active voucher
stableAddress := config.DefaultStableVoucherAddress()
stableDecimals := config.DefaultStableVoucherDecimals()
r, err := h.accountService.GetPoolSwapQuote(ctx, finalAmountStr, string(publicKey), string(activeAddress), string(activePoolAddress), stableAddress) // swap active -> FIRST stable from pool list
r, err := h.accountService.GetPoolSwapQuote(ctx, finalAmountStr, string(publicKey), string(activeAddress), string(activePoolAddress), firstPoolStable.TokenAddress)
if err != nil { if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.") res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err) logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil return res, nil
} }
finalQuote := store.ScaleDownBalance(r.OutValue, stableDecimals) // scale using REAL stable decimals
finalQuote := store.ScaleDownBalance(r.OutValue, firstPoolStable.TokenDecimals)
scaledCredit = store.AddDecimalStrings(scaledCredit, finalQuote) scaledCredit = store.AddDecimalStrings(scaledCredit, finalQuote)
}
for _, v := range stable { // 3. Add ALL wallet stable balances (from FetchVouchers)
for _, v := range allVouchers {
if isStableVoucher(v.TokenAddress) {
scaled := store.ScaleDownBalance(v.Balance, v.TokenDecimals) scaled := store.ScaleDownBalance(v.Balance, v.TokenDecimals)
scaledCredit = store.AddDecimalStrings(scaledCredit, scaled) scaledCredit = store.AddDecimalStrings(scaledCredit, scaled)
} }
}
// DEBT calculation: All outstanding active token that is in the current pool // DEBT calculation
// (how much of AT that is in the active Pool) // Rule:
// - Default = 0
// - If active is stable → remain 0
// - If active is non-stable and exists in pool → use pool balance
scaledDebt := "0"
if !isStableVoucher(string(activeAddress)) {
for _, v := range swappableVouchers {
if v.TokenSymbol == string(activeSym) {
scaledDebt = store.ScaleDownBalance(v.Balance, v.TokenDecimals)
break
}
}
}
formattedDebt, _ := store.TruncateDecimalString(scaledDebt, 2)
// Fetch MPESA rates // Fetch MPESA rates
rates, err := h.accountService.GetMpesaOnrampRates(ctx) rates, err := h.accountService.GetMpesaOnrampRates(ctx)
@ -250,9 +231,7 @@ func (h *MenuHandlers) CalculateCreditAndDebt(ctx context.Context, sym string, i
} }
creditFloat, _ := strconv.ParseFloat(scaledCredit, 64) creditFloat, _ := strconv.ParseFloat(scaledCredit, 64)
creditKsh := fmt.Sprintf("%f", creditFloat*rates.Buy) creditKsh := fmt.Sprintf("%f", creditFloat*rates.Buy)
kshFormattedCredit, _ := store.TruncateDecimalString(creditKsh, 0) kshFormattedCredit, _ := store.TruncateDecimalString(creditKsh, 0)
res.Content = l.Get( res.Content = l.Get(

View File

@ -36,7 +36,7 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
inputStr := string(input) inputStr := string(input)
if inputStr == "0" || inputStr == "99" || inputStr == "88" || inputStr == "98" { if inputStr == "0" || inputStr == "99" || inputStr == "88" || inputStr == "98" {
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error, flag_incorrect_voucher) res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error, flag_incorrect_voucher, flag_incorrect_pool)
return res, nil return res, nil
} }
@ -100,11 +100,13 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
} }
// Fetch min withdrawal amount from config/env // Fetch min withdrawal amount from config/env
minksh := fmt.Sprintf("%f", config.MinMpesaWithdrawAmount()) minWithdraw := config.MinMpesaWithdrawAmount() // float64 (20)
minKshFormatted, _ := store.TruncateDecimalString(minksh, 0) minKshFormatted, _ := store.TruncateDecimalString(fmt.Sprintf("%f", minWithdraw), 0)
// If SAT is the same as RAT, return early with KSH format // If SAT is the same as RAT (default USDm),
if string(metadata.TokenAddress) == string(recipientActiveAddress) { // or if the voucher is a stable coin
// return early with KSH format
if string(metadata.TokenAddress) == string(recipientActiveAddress) || isStableVoucher(metadata.TokenAddress) {
txType = "normal" txType = "normal"
// Save the transaction type // Save the transaction type
if err := userStore.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte(txType)); err != nil { if err := userStore.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte(txType)); err != nil {
@ -113,9 +115,16 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
} }
activeFloat, _ := strconv.ParseFloat(string(metadata.Balance), 64) activeFloat, _ := strconv.ParseFloat(string(metadata.Balance), 64)
ksh := fmt.Sprintf("%f", activeFloat*rates.Buy) kshValue := activeFloat * rates.Buy
maxKshFormatted, _ := store.TruncateDecimalString(ksh, 0) maxKshFormatted, _ := store.TruncateDecimalString(fmt.Sprintf("%f", kshValue), 0)
// Ensure that the max is greater than the min
if kshValue < minWithdraw {
res.FlagSet = append(res.FlagSet, flag_low_swap_amount)
res.Content = l.Get("%s Ksh", maxKshFormatted)
return res, nil
}
res.Content = l.Get( res.Content = l.Get(
"Enter the amount of Mpesa to withdraw: (Min: Ksh %s, Max %s Ksh)\n", "Enter the amount of Mpesa to withdraw: (Min: Ksh %s, Max %s Ksh)\n",
@ -123,6 +132,8 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
maxKshFormatted, maxKshFormatted,
) )
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error, flag_incorrect_voucher, flag_incorrect_pool)
return res, nil return res, nil
} }
@ -142,6 +153,7 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
if !canSwap.CanSwapFrom { // pool issue (CATCH on .vis) if !canSwap.CanSwapFrom { // pool issue (CATCH on .vis)
res.FlagSet = append(res.FlagSet, flag_incorrect_pool) res.FlagSet = append(res.FlagSet, flag_incorrect_pool)
res.Content = "0"
return res, nil return res, nil
} }
@ -149,10 +161,13 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
_, maxRAT, err := h.calculateSendCreditLimits(ctx, activePoolAddress, []byte(metadata.TokenAddress), recipientActiveAddress, publicKey, []byte(metadata.TokenDecimals), recipientActiveDecimal) _, maxRAT, err := h.calculateSendCreditLimits(ctx, activePoolAddress, []byte(metadata.TokenAddress), recipientActiveAddress, publicKey, []byte(metadata.TokenDecimals), recipientActiveDecimal)
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = "0"
logg.ErrorCtxf(ctx, "failed on calculateSendCreditLimits", "error", err) logg.ErrorCtxf(ctx, "failed on calculateSendCreditLimits", "error", err)
return res, nil return res, nil
} }
res.FlagReset = append(res.FlagReset, flag_api_call_error)
// Fallback if below minimum // Fallback if below minimum
maxFloat, _ := strconv.ParseFloat(maxRAT, 64) maxFloat, _ := strconv.ParseFloat(maxRAT, 64)
if maxFloat < 0.1 { if maxFloat < 0.1 {
@ -162,6 +177,8 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
return res, nil return res, nil
} }
res.FlagReset = append(res.FlagReset, flag_low_swap_amount)
// Save max RAT amount to be used in validating the user's input // 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)) err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, []byte(maxRAT))
if err != nil { if err != nil {
@ -198,6 +215,8 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
maxKshFormatted, maxKshFormatted,
) )
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error, flag_incorrect_voucher, flag_incorrect_pool)
return res, nil return res, nil
} }
@ -525,7 +544,7 @@ func (h *MenuHandlers) SendMpesaMinLimit(ctx context.Context, sym string, input
kshFormatted, _ := store.TruncateDecimalString(ksh, 0) kshFormatted, _ := store.TruncateDecimalString(ksh, 0)
res.Content = l.Get( res.Content = l.Get(
"Enter the amount of credit to receive: (Minimum %s Ksh)\n", "Enter the amount of credit to deposit: (Minimum %s Ksh)\n",
kshFormatted, kshFormatted,
) )
@ -597,7 +616,7 @@ func (h *MenuHandlers) SendMpesaPreview(ctx context.Context, sym string, input [
defaultAsset := config.DefaultMpesaAsset() defaultAsset := config.DefaultMpesaAsset()
res.Content = l.Get( res.Content = l.Get(
"You will get a prompt for your M-Pesa PIN shortly to send %s ksh and receive ~ %s %s", "You will get a prompt for your Mpesa PIN shortly to send %s ksh and receive ~ %s %s",
inputStr, estimateFormatted, defaultAsset, inputStr, estimateFormatted, defaultAsset,
) )

View File

@ -43,7 +43,7 @@ func (h *MenuHandlers) CalculateMaxPayDebt(ctx context.Context, sym string, inpu
} }
// Resolve active pool // Resolve active pool
activePoolAddress, activePoolName, err := h.resolveActivePoolDetails(ctx, sessionId) activePoolAddress, activePoolSymbol, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil { if err != nil {
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error) res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error)
return res, err return res, err
@ -64,6 +64,7 @@ func (h *MenuHandlers) CalculateMaxPayDebt(ctx context.Context, sym string, inpu
r, err := h.accountService.GetSwapFromTokenMaxLimit(ctx, string(activePoolAddress), metadata.TokenAddress, string(activeAddress), string(publicKey)) r, err := h.accountService.GetSwapFromTokenMaxLimit(ctx, string(activePoolAddress), metadata.TokenAddress, string(activeAddress), string(publicKey))
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = "0"
logg.ErrorCtxf(ctx, "failed on GetSwapFromTokenMaxLimit", "error", err) logg.ErrorCtxf(ctx, "failed on GetSwapFromTokenMaxLimit", "error", err)
return res, nil return res, nil
} }
@ -126,10 +127,10 @@ func (h *MenuHandlers) CalculateMaxPayDebt(ctx context.Context, sym string, inpu
quoteStr, _ := store.TruncateDecimalString(string(quoteAmountStr), 2) quoteStr, _ := store.TruncateDecimalString(string(quoteAmountStr), 2)
res.Content = l.Get( res.Content = l.Get(
"You can remove a max of %s %s from '%s'\nEnter amount of %s:(Max: %s)", "You can remove a max of %s %s from '%s' pool\nEnter amount of %s:(Max: %s)",
quoteStr, quoteStr,
string(activeSym), string(activeSym),
string(activePoolName), string(activePoolSymbol),
metadata.TokenSymbol, metadata.TokenSymbol,
maxStr, maxStr,
) )
@ -237,7 +238,7 @@ func (h *MenuHandlers) ConfirmDebtRemoval(ctx context.Context, sym string, input
} }
res.Content = l.Get( res.Content = l.Get(
"Please confirm that you will use %s %s to remove your debt of %s %s\n", "Please confirm that you will use %s %s to remove your debt of %s %s\nEnter your PIN:",
inputStr, payDebtVoucher.TokenSymbol, qouteStr, string(activeSym), inputStr, payDebtVoucher.TokenSymbol, qouteStr, string(activeSym),
) )
@ -268,7 +269,7 @@ func (h *MenuHandlers) InitiatePayDebt(ctx context.Context, sym string, input []
} }
// Resolve active pool // Resolve active pool
activePoolAddress, activePoolName, err := h.resolveActivePoolDetails(ctx, sessionId) activePoolAddress, activePoolSymbol, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil { if err != nil {
return res, err return res, err
} }
@ -310,7 +311,7 @@ func (h *MenuHandlers) InitiatePayDebt(ctx context.Context, sym string, input []
"Your request has been sent. You will receive an SMS when your debt of %s %s has been removed from %s.", "Your request has been sent. You will receive an SMS when your debt of %s %s has been removed from %s.",
string(debtQuotedAmount), string(debtQuotedAmount),
string(activeSym), string(activeSym),
activePoolName, activePoolSymbol,
) )
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)

View File

@ -145,14 +145,14 @@ func (h *MenuHandlers) ConfirmPoolDeposit(ctx context.Context, sym string, input
} }
// Resolve active pool // Resolve active pool
_, activePoolName, err := h.resolveActivePoolDetails(ctx, sessionId) _, activePoolSymbol, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil { if err != nil {
return res, err return res, err
} }
res.Content = l.Get( res.Content = l.Get(
"You will deposit %s %s into %s\n", "You will deposit %s %s into %s\n",
inputStr, poolDepositVoucher.TokenSymbol, activePoolName, inputStr, poolDepositVoucher.TokenSymbol, activePoolSymbol,
) )
return res, nil return res, nil
@ -173,9 +173,12 @@ func (h *MenuHandlers) InitiatePoolDeposit(ctx context.Context, sym string, inpu
l := gotext.NewLocale(translationDir, code) l := gotext.NewLocale(translationDir, code)
l.AddDomain("default") l.AddDomain("default")
userStore := h.userdataStore
// Resolve active pool // Resolve active pool
activePoolAddress, activePoolName, err := h.resolveActivePoolDetails(ctx, sessionId) activePoolAddress, activePoolSymbol, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed on resolveActivePoolDetails", "error", err)
return res, err return res, err
} }
@ -185,18 +188,26 @@ func (h *MenuHandlers) InitiatePoolDeposit(ctx context.Context, sym string, inpu
return res, err return res, err
} }
poolDepositdata, err := store.ReadTransactionData(ctx, h.userdataStore, sessionId) publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry", "key", storedb.DATA_PUBLIC_KEY, "error", err)
return res, err return res, err
} }
finalAmountStr, err := store.ParseAndScaleAmount(poolDepositdata.Amount, poolDepositVoucher.TokenDecimals) amount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_AMOUNT)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to read amount entry", "key", storedb.DATA_AMOUNT, "error", err)
return res, err return res, err
} }
// Call pool deposit API finalAmountStr, err := store.ParseAndScaleAmount(string(amount), poolDepositVoucher.TokenDecimals)
r, err := h.accountService.PoolDeposit(ctx, finalAmountStr, poolDepositdata.PublicKey, string(activePoolAddress), poolDepositVoucher.TokenAddress) if err != nil {
logg.ErrorCtxf(ctx, "failed on ParseAndScaleAmount", "error", err)
return res, err
}
// Call token transfer API and send the token to the pool address
r, err := h.accountService.TokenTransfer(ctx, finalAmountStr, string(publicKey), string(activePoolAddress), poolDepositVoucher.TokenAddress)
if err != nil { if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
@ -210,9 +221,9 @@ func (h *MenuHandlers) InitiatePoolDeposit(ctx context.Context, sym string, inpu
res.Content = l.Get( res.Content = l.Get(
"Your request has been sent. You will receive an SMS when %s %s has been deposited into %s.", "Your request has been sent. You will receive an SMS when %s %s has been deposited into %s.",
poolDepositdata.Amount, string(amount),
poolDepositVoucher.TokenSymbol, poolDepositVoucher.TokenSymbol,
activePoolName, activePoolSymbol,
) )
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)

View File

@ -3,6 +3,7 @@ package application
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
@ -104,6 +105,7 @@ func (h *MenuHandlers) GetDefaultPool(ctx context.Context, sym string, input []b
// ViewPool retrieves the pool details from the user store // ViewPool retrieves the pool details from the user store
// and displays it to the user for them to select it. // and displays it to the user for them to select it.
// if the data does not exist, it calls the API to get the pool details
func (h *MenuHandlers) ViewPool(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *MenuHandlers) ViewPool(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
@ -131,8 +133,11 @@ func (h *MenuHandlers) ViewPool(ctx context.Context, sym string, input []byte) (
if poolData == nil { if poolData == nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
// convert to uppercase before the call
poolSymbol := strings.ToUpper(inputStr)
// no match found. Call the API using the inputStr as the symbol // no match found. Call the API using the inputStr as the symbol
poolResp, err := h.accountService.RetrievePoolDetails(ctx, inputStr) poolResp, err := h.accountService.RetrievePoolDetails(ctx, poolSymbol)
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
return res, nil return res, nil

View File

@ -175,6 +175,10 @@ func (h *MenuHandlers) SwapMaxLimit(ctx context.Context, sym string, input []byt
return res, nil return res, nil
} }
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore userStore := h.userdataStore
metadata, err := store.GetSwapToVoucherData(ctx, userStore, sessionId, inputStr) metadata, err := store.GetSwapToVoucherData(ctx, userStore, sessionId, inputStr)
if err != nil { if err != nil {
@ -235,9 +239,9 @@ func (h *MenuHandlers) SwapMaxLimit(ctx context.Context, sym string, input []byt
return res, err return res, err
} }
res.Content = fmt.Sprintf( res.Content = l.Get(
"Maximum: %s\n\nEnter amount of %s to swap for %s:", "Maximum: %s %s\n\nEnter amount of %s to swap for %s:",
maxStr, swapData.ActiveSwapFromSym, swapData.ActiveSwapToSym, maxStr, swapData.ActiveSwapFromSym, swapData.ActiveSwapFromSym, swapData.ActiveSwapToSym,
) )
return res, nil return res, nil
@ -323,8 +327,8 @@ func (h *MenuHandlers) SwapPreview(ctx context.Context, sym string, input []byte
// Format to 2 decimal places // Format to 2 decimal places
qouteStr, _ := store.TruncateDecimalString(string(quoteAmountStr), 2) qouteStr, _ := store.TruncateDecimalString(string(quoteAmountStr), 2)
res.Content = fmt.Sprintf( res.Content = l.Get(
"You will swap:\n%s %s for %s %s:", "You will swap %s %s for %s %s:",
inputStr, swapData.ActiveSwapFromSym, qouteStr, swapData.ActiveSwapToSym, inputStr, swapData.ActiveSwapFromSym, qouteStr, swapData.ActiveSwapToSym,
) )

View File

@ -380,6 +380,19 @@ func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte)
return res, err return res, err
} }
// Case for M-Pesa
// if the recipient is Mpesa (address), check if the sender's voucher is a stable coin
recipientAddress, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to read recipient's address", "error", err)
return res, err
}
if string(recipientAddress) == config.DefaultMpesaAddress() && isStableVoucher(string(activeAddress)) {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
res.Content = l.Get("Maximum amount: %s %s\nEnter amount:", formattedBalance, string(activeSym))
return res, nil
}
if string(swapToVoucher.TokenAddress) == string(activeAddress) { if string(swapToVoucher.TokenAddress) == string(activeAddress) {
// recipient has active token same as selected token → normal transaction // recipient has active token same as selected token → normal transaction
transactionType = []byte("normal") transactionType = []byte("normal")
@ -414,12 +427,15 @@ func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
// Get the recipient's phone number to read other data items // Get the recipient's phone number to read other data items (*)
recipientPhoneNumber, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER) recipientPhoneNumber, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER)
if err != nil { if err != nil || !phone.IsValidPhoneNumber(string(recipientPhoneNumber)) {
// invalid state // revert to normal transaction
return res, 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
} }
recipientActiveSym, recipientActiveAddress, recipientActiveDecimal, err := h.getRecipientData(ctx, string(recipientPhoneNumber)) recipientActiveSym, recipientActiveAddress, recipientActiveDecimal, err := h.getRecipientData(ctx, string(recipientPhoneNumber))
if err != nil { if err != nil {
return res, err return res, err
@ -529,7 +545,7 @@ func (h *MenuHandlers) getRecipientData(ctx context.Context, sessionId string) (
return return
} }
func (h *MenuHandlers) resolveActivePoolDetails(ctx context.Context, sessionId string) (defaultPoolAddress, defaultPoolName []byte, err error) { func (h *MenuHandlers) resolveActivePoolDetails(ctx context.Context, sessionId string) (defaultPoolAddress, defaultPoolSymbol []byte, err error) {
store := h.userdataStore store := h.userdataStore
// Try read address // Try read address
@ -539,21 +555,21 @@ func (h *MenuHandlers) resolveActivePoolDetails(ctx context.Context, sessionId s
return nil, nil, err return nil, nil, err
} }
// Try read name // Try read symbol
defaultPoolName, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_NAME) defaultPoolSymbol, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_SYM)
if err != nil && !db.IsNotFound(err) { if err != nil && !db.IsNotFound(err) {
logg.ErrorCtxf(ctx, "failed to read active pool name", "error", err) logg.ErrorCtxf(ctx, "failed to read active pool name", "error", err)
return nil, nil, err return nil, nil, err
} }
// If both exist, return them // If both exist, return them
if defaultPoolAddress != nil && defaultPoolName != nil { if defaultPoolAddress != nil && defaultPoolSymbol != nil {
return defaultPoolAddress, defaultPoolName, nil return defaultPoolAddress, defaultPoolSymbol, nil
} }
// Fallback to config defaults // Fallback to config defaults
defaultPoolAddress = []byte(config.DefaultPoolAddress()) defaultPoolAddress = []byte(config.DefaultPoolAddress())
defaultPoolName = []byte(config.DefaultPoolName()) defaultPoolSymbol = []byte(config.DefaultPoolSymbol())
if err := store.WriteEntry( if err := store.WriteEntry(
ctx, ctx,
@ -568,14 +584,14 @@ func (h *MenuHandlers) resolveActivePoolDetails(ctx context.Context, sessionId s
if err := store.WriteEntry( if err := store.WriteEntry(
ctx, ctx,
sessionId, sessionId,
storedb.DATA_ACTIVE_POOL_NAME, storedb.DATA_ACTIVE_POOL_SYM,
defaultPoolName, defaultPoolSymbol,
); err != nil { ); err != nil {
logg.ErrorCtxf(ctx, "failed to write default pool name", "error", err) logg.ErrorCtxf(ctx, "failed to write default pool symbol", "error", err)
return nil, nil, err return nil, nil, err
} }
return defaultPoolAddress, defaultPoolName, nil return defaultPoolAddress, defaultPoolSymbol, nil
} }
func (h *MenuHandlers) calculateSendCreditLimits(ctx context.Context, poolAddress, fromAddress, toAddress, publicKey, fromDecimal, toDecimal []byte) (string, string, error) { func (h *MenuHandlers) calculateSendCreditLimits(ctx context.Context, poolAddress, fromAddress, toAddress, publicKey, fromDecimal, toDecimal []byte) (string, string, error) {
@ -956,25 +972,18 @@ func (h *MenuHandlers) TransactionSwapPreview(ctx context.Context, sym string, i
} }
sendInputAmount := r.InputAmount // amount of SAT that should be swapped 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) // store the finalAmountStr as the final amount (that will be sent after the swap)
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(sendOutputAmount)) err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(finalAmountStr))
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to write output amount value entry with", "key", storedb.DATA_AMOUNT, "value", sendOutputAmount, "error", err) logg.ErrorCtxf(ctx, "failed to write output amount value entry with", "key", storedb.DATA_AMOUNT, "value", finalAmountStr, "error", err)
return res, err return res, err
} }
// Scale down the quoted output amount // store the qouteAmount in the temporary value
quoteAmountStr := store.ScaleDownBalance(sendOutputAmount, swapToVoucher.TokenDecimals) err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(inputStr))
// Format the qouteAmount amount to 2 decimal places
qouteAmount, _ := store.TruncateDecimalString(quoteAmountStr, 2)
// store the qouteAmount in the temporary value (display)
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(qouteAmount))
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporary qouteAmount entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", qouteAmount, "error", err) logg.ErrorCtxf(ctx, "failed to write temporary inputStr entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", inputStr, "error", err)
return res, err return res, err
} }
@ -988,7 +997,7 @@ func (h *MenuHandlers) TransactionSwapPreview(ctx context.Context, sym string, i
res.Content = l.Get( res.Content = l.Get(
"%s will receive %s %s", "%s will receive %s %s",
string(recipientInput), string(recipientInput),
qouteAmount, inputStr,
swapToVoucher.TokenSymbol, swapToVoucher.TokenSymbol,
) )

View File

@ -17,8 +17,8 @@ import (
// ManageVouchers retrieves the token holdings from the API using the "PublicKey" and // ManageVouchers retrieves the token holdings from the API using the "PublicKey" and
// 1. sets the first as the default voucher if no active voucher is set. // 1. sets the first as the default voucher if no active voucher is set.
// 2. Stores list of vouchers // 2. Stores list of filtered ordered vouchers (exclude the active voucher)
// 3. Stores list of filtered stable vouchers // 3. Stores list of ordered vouchers (all vouchers)
// 4. updates the balance of the active voucher // 4. updates the balance of the active voucher
func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -32,7 +32,6 @@ func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []b
flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher") flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_no_stable_vouchers, _ := h.flagManager.GetFlag("flag_no_stable_vouchers")
flag_multiple_voucher, _ := h.flagManager.GetFlag("flag_multiple_voucher") flag_multiple_voucher, _ := h.flagManager.GetFlag("flag_multiple_voucher")
publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY) publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
@ -216,21 +215,6 @@ func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []b
// Order all vouchers // Order all vouchers
orderedVouchers := orderVouchers(vouchersResp) orderedVouchers := orderVouchers(vouchersResp)
// Stable voucher presence flag (based on full list)
hasStable := false
for _, v := range orderedVouchers {
if isStableVoucher(v.TokenAddress) {
hasStable = true
break
}
}
if !hasStable {
res.FlagSet = append(res.FlagSet, flag_no_stable_vouchers)
} else {
res.FlagReset = append(res.FlagReset, flag_no_stable_vouchers)
}
// Process ALL vouchers (stable first) // Process ALL vouchers (stable first)
orderedVoucherData := store.ProcessVouchers(orderedVouchers) orderedVoucherData := store.ProcessVouchers(orderedVouchers)
@ -253,6 +237,7 @@ func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []b
} }
// GetVoucherList fetches the list of vouchers from the store and formats them. // GetVoucherList fetches the list of vouchers from the store and formats them.
// does not include the active voucher and is used in select_voucher and pay_debt
func (h *MenuHandlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *MenuHandlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
@ -281,7 +266,12 @@ func (h *MenuHandlers) GetVoucherList(ctx context.Context, sym string, input []b
} }
if len(voucherData) == 0 { if len(voucherData) == 0 {
if sym == "get_paydebt_voucher_list" {
res.Content = l.Get("You need another voucher to proceed. Only found %s.", string(activeSym))
} else {
res.Content = l.Get("Your active voucher %s is already set", string(activeSym)) res.Content = l.Get("Your active voucher %s is already set", string(activeSym))
}
return res, nil return res, nil
} }

View File

@ -111,7 +111,8 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService)
ls.DbRs.AddLocalFunc("quit_with_help", appHandlers.QuitWithHelp) ls.DbRs.AddLocalFunc("quit_with_help", appHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_community_balance", appHandlers.FetchCommunityBalance) ls.DbRs.AddLocalFunc("fetch_community_balance", appHandlers.FetchCommunityBalance)
ls.DbRs.AddLocalFunc("manage_vouchers", appHandlers.ManageVouchers) ls.DbRs.AddLocalFunc("manage_vouchers", appHandlers.ManageVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", appHandlers.GetVoucherList) ls.DbRs.AddLocalFunc("get_voucher_list", appHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("get_paydebt_voucher_list", appHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("view_voucher", appHandlers.ViewVoucher) ls.DbRs.AddLocalFunc("view_voucher", appHandlers.ViewVoucher)
ls.DbRs.AddLocalFunc("set_voucher", appHandlers.SetVoucher) ls.DbRs.AddLocalFunc("set_voucher", appHandlers.SetVoucher)
ls.DbRs.AddLocalFunc("get_voucher_details", appHandlers.GetVoucherDetails) ls.DbRs.AddLocalFunc("get_voucher_details", appHandlers.GetVoucherDetails)

View File

@ -1,2 +1 @@
{{.confirm_debt_removal}} {{.confirm_debt_removal}}
Enter your PIN:

View File

@ -10,6 +10,10 @@ INCMP > 88
INCMP < 98 INCMP < 98
INCMP _ 0 INCMP _ 0
INCMP quit 99 INCMP quit 99
LOAD get_mpesa_max_limit 0 LOAD get_mpesa_max_limit 89
RELOAD get_mpesa_max_limit RELOAD get_mpesa_max_limit
CATCH . flag_incorrect_voucher 1
CATCH low_withdraw_mpesa_amount flag_incorrect_pool 1
CATCH low_withdraw_mpesa_amount flag_low_swap_amount 1
CATCH low_withdraw_mpesa_amount flag_api_call_error 1
INCMP mpesa_max_limit * INCMP mpesa_max_limit *

View File

@ -59,7 +59,7 @@ msgid "Enter the amount of M-Pesa to get: (Max %s Ksh)\n"
msgstr "Weka kiasi cha M-Pesa cha kupata: (Kikomo %s Ksh)\n" msgstr "Weka kiasi cha M-Pesa cha kupata: (Kikomo %s Ksh)\n"
msgid "You are sending %s %s in order to receive ~ %s ksh" msgid "You are sending %s %s in order to receive ~ %s ksh"
msgstr "Unatuma ~ %s %s ili upoke %s ksh" msgstr "Unatuma ~ %s %s ili upokee %s ksh"
msgid "Your request has been sent. Please await confirmation" msgid "Your request has been sent. Please await confirmation"
msgstr "Ombi lako limetumwa. Tafadhali subiri" msgstr "Ombi lako limetumwa. Tafadhali subiri"
@ -67,16 +67,16 @@ msgstr "Ombi lako limetumwa. Tafadhali subiri"
msgid "Enter the amount of M-Pesa to send: (Minimum %s Ksh)\n" msgid "Enter the amount of M-Pesa to send: (Minimum %s Ksh)\n"
msgstr "Weka kiasi cha M-Pesa cha kutuma: (Kima cha chini %s Ksh)\n" msgstr "Weka kiasi cha M-Pesa cha kutuma: (Kima cha chini %s Ksh)\n"
msgid "You will get a prompt for your M-Pesa PIN shortly to send %s ksh and receive ~ %s cUSD" msgid "You will get a prompt for your Mpesa PIN shortly to send %s ksh and receive ~ %s %s"
msgstr "Utapokea kidokezo cha PIN yako ya M-Pesa hivi karibuni kutuma %s ksh na kupokea ~ %s cUSD" msgstr "Utapokea kidokezo cha PIN yako ya Mpesa hivi karibuni kutuma %s ksh na kupokea ~ %s %s"
msgid "Your request has been sent. Thank you for using Sarafu" msgid "Your request has been sent. Thank you for using Sarafu"
msgstr "Ombi lako limetumwa. Asante kwa kutumia huduma ya Sarafu" msgstr "Ombi lako limetumwa. Asante kwa kutumia huduma ya Sarafu"
msgid "You can remove a maximum of %s %s from '%s' pool\n\nEnter amount of %s:" msgid "You can remove a max of %s %s from '%s' pool\nEnter amount of %s:(Max: %s)"
msgstr "Unaweza kuondoa kiwango cha juu cha %s %s kutoka kwenye '%s'\n\nWeka kiwango cha %s:" msgstr "Unaweza kuondoa kiwango cha juu cha %s %s kutoka kwenye '%s'\n\nWeka kiwango cha %s:(Kikomo: %s)"
msgid "Please confirm that you will use %s %s to remove your debt of %s %s\n" msgid "Please confirm that you will use %s %s to remove your debt of %s %s\nEnter your PIN:"
msgstr "Tafadhali thibitisha kwamba utatumia %s %s kulipa deni lako la %s %s.\nWeka PIN yako:" msgstr "Tafadhali thibitisha kwamba utatumia %s %s kulipa deni lako la %s %s.\nWeka PIN yako:"
msgid "Your active voucher %s is already set" msgid "Your active voucher %s is already set"
@ -93,3 +93,21 @@ msgstr "Ombi lako limetumwa. Utapokea ujumbe wakati %s %s itawekwa kwenye %s."
msgid "%s will receive %s %s from %s" msgid "%s will receive %s %s from %s"
msgstr %s atapokea %s %s kutoka kwa %s" msgstr %s atapokea %s %s kutoka kwa %s"
msgid "You need another voucher to proceed. Only found %s."
msgstr "Unahitaji kua na sarafu nyingine. Tumepata tu %s."
msgid "Maximum: %s %s\n\nEnter amount of %s to swap for %s:"
msgstr "Kikimo: %s %s\n\nWeka kiasi cha %s kitakacho badilishwa kua %s:"
msgid "You will swap %s %s for %s %s:"
msgstr "Utabadilisha %s %s kua %s %s:"
msgid "Your request has been sent. You will receive an SMS when your debt of %s %s has been removed from %s."
msgstr "Ombi lako limetumwa. Utapokea ujumbe wakati deni lako la %s %s litatolewa kwa %s."
msgid "Enter the amount of Mpesa to withdraw: (Min: Ksh %s, Max %s Ksh)\n"
msgstr "Weka kiasi cha Mpesa utakacho toa: (Min: Ksh %s, Max %s Ksh)\n"
msgid "Enter the amount of credit to deposit: (Minimum %s Ksh)\n"
msgstr "Weka kiasi utakacho weka (Kima cha chini: %s Ksh)\n"

View File

@ -1 +1 @@
You have a low debt amount Available amount {{.calculate_max_pay_debt}} is too low, please choose a different voucher:

View File

@ -1,5 +1,6 @@
MAP calculate_max_pay_debt
MOUT back 0 MOUT back 0
MOUT quit 9 MOUT quit 9
HALT HALT
INCMP ^ 0 INCMP _ 0
INCMP quit 9 INCMP quit 9

View File

@ -1 +1 @@
Kiasi cha deni lako ni cha chini sana Kiasi kinachopatikana {{.calculate_max_pay_debt}} ni cha chini sana, tafadhali chagua sarafu tofauti:

View File

@ -1 +1 @@
Available amount {{.swap_max_limit}} is too low, please try again: Available amount {{.swap_max_limit}} is too low, please choose a different voucher:

View File

@ -1 +1 @@
Kiasi kinachopatikana {{.swap_max_limit}} ni cha chini sana, tafadhali jaribu tena: Kiasi kinachopatikana {{.swap_max_limit}} ni cha chini sana, tafadhali chagua sarafu tofauti:

View File

@ -0,0 +1 @@
Available amount {{.get_mpesa_max_limit}} is too low, please choose a different voucher:

View File

@ -0,0 +1,6 @@
MAP get_mpesa_max_limit
MOUT back 0
MOUT quit 9
HALT
INCMP _ 0
INCMP quit 9

View File

@ -0,0 +1 @@
Kiasi kinachopatikana {{.get_mpesa_max_limit}} ni cha chini sana, tafadhali chagua sarafu tofauti:

View File

@ -1 +1 @@
{{.get_vouchers}} {{.get_paydebt_voucher_list}}

View File

@ -1,6 +1,6 @@
CATCH no_voucher flag_no_active_voucher 1 CATCH no_voucher flag_no_active_voucher 1
LOAD get_vouchers 0 LOAD get_paydebt_voucher_list 0
MAP get_vouchers MAP get_paydebt_voucher_list
MOUT back 0 MOUT back 0
MOUT quit 99 MOUT quit 99
MNEXT next 88 MNEXT next 88
@ -13,4 +13,6 @@ INCMP quit 99
LOAD calculate_max_pay_debt 0 LOAD calculate_max_pay_debt 0
RELOAD calculate_max_pay_debt RELOAD calculate_max_pay_debt
CATCH . flag_incorrect_voucher 1 CATCH . flag_incorrect_voucher 1
CATCH low_pay_debt_amount flag_low_swap_amount 1
CATCH low_pay_debt_amount flag_api_call_error 1
INCMP calculate_max_pay_debt * INCMP calculate_max_pay_debt *

View File

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

View File

@ -1,5 +1,4 @@
CATCH no_voucher flag_no_active_voucher 1 CATCH no_voucher flag_no_active_voucher 1
CATCH no_stable_voucher flag_no_stable_vouchers 1
LOAD get_ordered_vouchers 0 LOAD get_ordered_vouchers 0
MAP get_ordered_vouchers MAP get_ordered_vouchers
MOUT back 0 MOUT back 0

View File

@ -1 +1 @@
{{.get_vouchers}} {{.get_voucher_list}}

View File

@ -1,6 +1,6 @@
CATCH no_voucher flag_no_active_voucher 1 CATCH no_voucher flag_no_active_voucher 1
LOAD get_vouchers 0 LOAD get_voucher_list 0
MAP get_vouchers MAP get_voucher_list
MOUT back 0 MOUT back 0
MOUT quit 99 MOUT quit 99
MNEXT next 88 MNEXT next 88

View File

@ -1,3 +1,2 @@
{{.send_mpesa_preview}} {{.send_mpesa_preview}}
Enter your PIN to confirm:
Please enter your account PIN to confirm:

View File

@ -1,3 +1,2 @@
{{.send_mpesa_preview}} {{.send_mpesa_preview}}
Weka PIN yako kudhibitisha:
Tafadhali weka PIN ya akaunti yako kudhibitisha:

View File

@ -12,7 +12,7 @@ INCMP > 88
INCMP < 98 INCMP < 98
INCMP _ 0 INCMP _ 0
INCMP quit 99 INCMP quit 99
LOAD swap_max_limit 64 LOAD swap_max_limit 138
RELOAD swap_max_limit RELOAD swap_max_limit
CATCH api_failure flag_api_call_error 1 CATCH api_failure flag_api_call_error 1
CATCH . flag_incorrect_voucher 1 CATCH . flag_incorrect_voucher 1

View File

@ -159,6 +159,10 @@ func GetOrderedVoucherData(ctx context.Context, store DataStore, sessionId strin
// MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input. // MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input.
func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) { func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) {
// case for invalid input with no current symbols
if symbols == "" {
return
}
symList := strings.Split(symbols, "\n") symList := strings.Split(symbols, "\n")
balList := strings.Split(balances, "\n") balList := strings.Split(balances, "\n")
decList := strings.Split(decimals, "\n") decList := strings.Split(decimals, "\n")