Merge pull request 'debt-menu' (#115) from debt-menu into master

Reviewed-on: #115
This commit is contained in:
Alfred Kamanda 2026-02-25 09:44:57 +01:00
commit 38f0058d0a
85 changed files with 1796 additions and 243 deletions

View File

@ -34,6 +34,12 @@ INCLUDE_STABLES_PARAM=false
DEFAULT_MPESA_ADDRESS=0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e
MIN_MPESA_SEND_AMOUNT=100
MAX_MPESA_SEND_AMOUNT=250000
MIN_MPESA_WITHDRAW_AMOUNT=20
DEFAULT_MPESA_ASSET=cUSD
MPESA_BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
MPESA_ONRAMP_BASE=https://pretium.v1.grassecon.net
# Known stable voucher addresses (USDm, USD₮)
STABLE_VOUCHER_ADDRESSES=0x765DE816845861e75A25fCA122bb6898B8B1282a,0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e
DEFAULT_STABLE_VOUCHER_ADDRESS=0x765DE816845861e75A25fCA122bb6898B8B1282a
DEFAULT_STABLE_VOUCHER_DECIMALS=18

View File

@ -102,6 +102,15 @@ func MinMpesaSendAmount() float64 {
return f
}
func MinMpesaWithdrawAmount() float64 {
v := env.GetEnv("MIN_MPESA_WITHDRAW_AMOUNT", "20")
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return 20 // fallback
}
return f
}
func MaxMpesaSendAmount() float64 {
v := env.GetEnv("MAX_MPESA_SEND_AMOUNT", "250000")
f, err := strconv.ParseFloat(v, 64)
@ -114,3 +123,30 @@ func MaxMpesaSendAmount() float64 {
func DefaultMpesaAsset() string {
return env.GetEnv("DEFAULT_MPESA_ASSET", "")
}
func StableVoucherAddresses() []string {
var parsed []string
raw := env.GetEnv("STABLE_VOUCHER_ADDRESSES", "")
if raw == "" {
return parsed
}
list := strings.Split(raw, ",")
for _, addr := range list {
clean := strings.TrimSpace(addr)
if clean != "" {
parsed = append(parsed, clean)
}
}
return parsed
}
func DefaultStableVoucherAddress() string {
return env.GetEnv("DEFAULT_STABLE_VOUCHER_ADDRESS", "")
}
func DefaultStableVoucherDecimals() string {
return env.GetEnv("DEFAULT_STABLE_VOUCHER_DECIMALS", "")
}

2
go.mod
View File

@ -7,7 +7,7 @@ toolchain go1.24.10
require (
git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66
git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20251127132814-8ceadabbc215
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251202085112-45469d4ba326
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20260207150752-71aa5ce7b537
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

2
go.sum
View File

@ -6,6 +6,8 @@ git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251128071248-
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251128071248-bfdeef125576/go.mod h1:h/y/lJNJAVTcIzAxCMXXw8Dh2aoLxBFZ6F1nTB8C0nU=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251202085112-45469d4ba326 h1:qH4QulgncvAD7b/YeHGPxcDJTBIychPeoZJACefYryI=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251202085112-45469d4ba326/go.mod h1:h/y/lJNJAVTcIzAxCMXXw8Dh2aoLxBFZ6F1nTB8C0nU=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20260207150752-71aa5ce7b537 h1:2AoOHiRTN3SXX4qnc2wOaF2ktVXLlFAa3X/n9DLu8/s=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20260207150752-71aa5ce7b537/go.mod h1:h/y/lJNJAVTcIzAxCMXXw8Dh2aoLxBFZ6F1nTB8C0nU=
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306 h1:Jo+yWysWw/N5BJQtAyEMN8ePVvAyPHv+JG4lQti+1N4=
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306/go.mod h1:FdLwYtzsjOIcDiW4uDgDYnB4Wdzq12uJUe0QHSSPbSo=
git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 h1:DjJlBSz0S13acft5XZDWk7ZYnzElym0xLMYEVgyNJ+E=

View File

@ -3,11 +3,13 @@ package application
import (
"context"
"fmt"
"strconv"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/sarafu-vise/store"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
"gopkg.in/leonelquinteros/gotext.v1"
)
@ -52,6 +54,7 @@ func (h *MenuHandlers) CheckBalance(ctx context.Context, sym string, input []byt
return res, err
}
}
content, err = loadUserContent(ctx, string(activeSym), string(activeBal), string(accAlias))
if err != nil {
return res, err
@ -62,7 +65,7 @@ func (h *MenuHandlers) CheckBalance(ctx context.Context, sym string, input []byt
}
// loadUserContent loads the main user content in the main menu: the alias, balance and active symbol associated with active voucher
func loadUserContent(ctx context.Context, activeSym string, balance string, alias string) (string, error) {
func loadUserContent(ctx context.Context, activeSym, balance, alias string) (string, error) {
var content string
code := codeFromCtx(ctx)
@ -75,8 +78,9 @@ func loadUserContent(ctx context.Context, activeSym string, balance string, alia
formattedAmount = "0.00"
}
// format the final output
// format the final outputs
balStr := fmt.Sprintf("%s %s", formattedAmount, activeSym)
if alias != "" {
content = l.Get("%s\nBalance: %s\n", alias, balStr)
} else {
@ -98,3 +102,144 @@ func (h *MenuHandlers) FetchCommunityBalance(ctx context.Context, sym string, in
res.Content = l.Get("Community Balance: 0.00")
return res, nil
}
// CalculateCreditAndDebt calls the API to get the credit and debt
// uses the pretium rates to convert the value to Ksh
func (h *MenuHandlers) CalculateCreditAndDebt(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")
}
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
// Fetch session data
_, activeBal, activeSym, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId)
if err != nil {
res.Content = l.Get("Credit: %s KSH\nDebt: %s %s\n", "0", "0", string(activeSym))
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_call_error)
// Resolve active pool
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
// Fetch swappable vouchers (pool view)
swappableVouchers, err := h.accountService.GetPoolSwappableFromVouchers(ctx, string(activePoolAddress), string(publicKey))
if err != nil {
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
}
if len(swappableVouchers) == 0 {
res.Content = l.Get("Credit: %s KSH\nDebt: %s %s\n", "0", "0", string(activeSym))
return res, nil
}
// Fetch ALL wallet vouchers (voucher holdings view)
allVouchers, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil {
logg.ErrorCtxf(ctx, "failed on FetchVouchers", "error", err)
return res, nil
}
// CREDIT calculation
// Rule:
// 1. Swap quote of active voucher → first stable in pool from GetPoolSwappableFromVouchers
// 2. PLUS all stable balances from FetchVouchers
scaledCredit := "0"
// 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 {
return res, err
}
// 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 {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil
}
// scale using REAL stable decimals
finalQuote := store.ScaleDownBalance(r.OutValue, firstPoolStable.TokenDecimals)
scaledCredit = store.AddDecimalStrings(scaledCredit, finalQuote)
}
// 3. Add ALL wallet stable balances (from FetchVouchers)
for _, v := range allVouchers {
if isStableVoucher(v.TokenAddress) {
scaled := store.ScaleDownBalance(v.Balance, v.TokenDecimals)
scaledCredit = store.AddDecimalStrings(scaledCredit, scaled)
}
}
// DEBT calculation
// 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
rates, err := h.accountService.GetMpesaOnrampRates(ctx)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on GetMpesaOnrampRates", "error", err)
return res, nil
}
creditFloat, _ := strconv.ParseFloat(scaledCredit, 64)
creditKsh := fmt.Sprintf("%f", creditFloat*rates.Buy)
kshFormattedCredit, _ := store.TruncateDecimalString(creditKsh, 0)
res.Content = l.Get(
"Credit: %s KSH\nDebt: %s %s\n",
kshFormattedCredit,
formattedDebt,
string(activeSym),
)
return res, nil
}

View File

@ -28,20 +28,36 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_low_swap_amount, _ := h.flagManager.GetFlag("flag_low_swap_amount")
flag_incorrect_pool, _ := h.flagManager.GetFlag("flag_incorrect_pool")
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
inputStr := string(input)
if inputStr == "0" || inputStr == "9" {
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, flag_incorrect_pool)
return res, nil
}
userStore := h.userdataStore
metadata, err := store.GetOrderedVoucherData(ctx, userStore, sessionId, inputStr)
if err != nil {
return res, fmt.Errorf("failed to retrieve swap to voucher data: %v", err)
}
if metadata == nil {
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher)
return res, nil
}
// Store the active transaction voucher data (from token)
if err := store.StoreTransactionVoucher(ctx, h.userdataStore, sessionId, metadata); err != nil {
logg.ErrorCtxf(ctx, "failed on StoreTransactionVoucher", "error", err)
return res, err
}
// Fetch session data
_, activeBal, _, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId)
_, _, _, _, publicKey, _, err := h.getSessionData(ctx, sessionId)
if err != nil {
return res, err
}
@ -77,14 +93,18 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
return res, err
}
// fetch data for verification
// fetch data for verification (to_voucher data)
recipientActiveSym, recipientActiveAddress, recipientActiveDecimal, err := h.getRecipientData(ctx, string(recipientPhoneNumber))
if err != nil {
return res, err
}
// If RAT is the same as SAT, return early with KSH format
if string(activeAddress) == string(recipientActiveAddress) {
// Fetch min withdrawal amount from config/env
minksh := fmt.Sprintf("%f", config.MinMpesaWithdrawAmount())
minKshFormatted, _ := store.TruncateDecimalString(minksh, 0)
// If SAT is the same as RAT, return early with KSH format
if string(metadata.TokenAddress) == string(recipientActiveAddress) {
txType = "normal"
// Save the transaction type
if err := userStore.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte(txType)); err != nil {
@ -92,46 +112,53 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
return res, err
}
activeFloat, _ := strconv.ParseFloat(string(activeBal), 64)
activeFloat, _ := strconv.ParseFloat(string(metadata.Balance), 64)
ksh := fmt.Sprintf("%f", activeFloat*rates.Buy)
kshFormatted, _ := store.TruncateDecimalString(ksh, 0)
maxKshFormatted, _ := store.TruncateDecimalString(ksh, 0)
res.Content = l.Get(
"Enter the amount of Mpesa to get: (Max %s Ksh)\n",
kshFormatted,
"Enter the amount of Mpesa to withdraw: (Min: Ksh %s, Max %s Ksh)\n",
minKshFormatted,
maxKshFormatted,
)
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error, flag_incorrect_voucher, flag_incorrect_pool)
return res, nil
}
// Resolve active pool address
activePoolAddress, err := h.resolveActivePoolAddress(ctx, sessionId)
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
// Check if sender token is swappable
canSwap, err := h.accountService.CheckTokenInPool(ctx, string(activePoolAddress), string(activeAddress))
// Check if selected token is swappable
canSwap, err := h.accountService.CheckTokenInPool(ctx, string(activePoolAddress), string(metadata.TokenAddress))
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on CheckTokenInPool", "error", err)
return res, nil
}
if !canSwap.CanSwapFrom { // pool issue (TODO on vis)
if !canSwap.CanSwapFrom { // pool issue (CATCH on .vis)
res.FlagSet = append(res.FlagSet, flag_incorrect_pool)
res.Content = "0"
return res, nil
}
// retrieve the max credit send amounts
_, maxRAT, err := h.calculateSendCreditLimits(ctx, activePoolAddress, activeAddress, recipientActiveAddress, publicKey, activeDecimal, recipientActiveDecimal)
_, maxRAT, err := h.calculateSendCreditLimits(ctx, activePoolAddress, []byte(metadata.TokenAddress), recipientActiveAddress, publicKey, []byte(metadata.TokenDecimals), recipientActiveDecimal)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = "0"
logg.ErrorCtxf(ctx, "failed on calculateSendCreditLimits", "error", err)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_call_error)
// Fallback if below minimum
maxFloat, _ := strconv.ParseFloat(maxRAT, 64)
if maxFloat < 0.1 {
@ -141,6 +168,8 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
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
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, []byte(maxRAT))
if err != nil {
@ -154,28 +183,31 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
return res, err
}
// save swap related data for the swap preview
metadata := &dataserviceapi.TokenHoldings{
// save swap related data for the swap preview (the swap to)
swapMetadata := &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 {
if err := store.UpdateSwapToVoucherData(ctx, userStore, sessionId, swapMetadata); err != nil {
logg.ErrorCtxf(ctx, "failed on UpdateSwapToVoucherData", "error", err)
return res, err
}
maxKsh := maxFloat * rates.Buy
kshStr := fmt.Sprintf("%f", maxKsh)
kshFormatted, _ := store.TruncateDecimalString(kshStr, 0)
maxKshFormatted, _ := store.TruncateDecimalString(kshStr, 0)
res.Content = l.Get(
"Enter the amount of Mpesa to get: (Max %s Ksh)\n",
kshFormatted,
"Enter the amount of Mpesa to withdraw: (Min: Ksh %s, Max %s Ksh)\n",
minKshFormatted,
maxKshFormatted,
)
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error, flag_incorrect_voucher, flag_incorrect_pool)
return res, nil
}
@ -189,7 +221,7 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
// INPUT IN RAT Ksh
inputStr := string(input)
if inputStr == "9" {
if inputStr == "0" || inputStr == "9" {
return res, nil
}
@ -219,17 +251,20 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
return res, nil
}
min := config.MinMpesaWithdrawAmount()
if kshAmount < min {
// if the input is below the minimum
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
// divide by the buy rate
inputAmount := kshAmount / rates.Buy
// store the user's raw input amount in the temporary value
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(inputStr))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporary inputStr entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", inputStr, "error", err)
return res, err
}
swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId)
// Resolve active pool
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
@ -239,19 +274,21 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
return res, err
}
if string(transactionType) == "normal" {
activeBal, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", storedb.DATA_ACTIVE_BAL, "error", err)
return res, err
}
balanceValue, err := strconv.ParseFloat(string(activeBal), 64)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to convert the activeBal to a float", "error", err)
return res, err
}
// get the selected voucher
mpesaWithdrawalVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
if inputAmount > balanceValue {
if string(transactionType) == "normal" {
// get the max based on the selected voucher balance
maxValue, err := strconv.ParseFloat(mpesaWithdrawalVoucher.Balance, 64)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to convert the stored balance string to a float", "error", err)
return res, err
}
if inputAmount > maxValue {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
@ -270,14 +307,26 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
res.Content = l.Get(
"You are sending %s %s in order to receive ~ %s ksh",
qouteInputAmount, swapData.ActiveSwapFromSym, inputStr,
qouteInputAmount, mpesaWithdrawalVoucher.TokenSymbol, inputStr,
)
return res, nil
}
swapToVoucher, err := store.ReadSwapToVoucher(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on ReadSwapFromVoucher", "error", err)
return res, err
}
swapMaxAmount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read swapMaxAmount entry with", "key", storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, "error", err)
return res, err
}
// use the stored max RAT
maxRATValue, err := strconv.ParseFloat(swapData.ActiveSwapMaxAmount, 64)
maxRATValue, err := strconv.ParseFloat(string(swapMaxAmount), 64)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to convert the swapMaxAmount to a float", "error", err)
return res, err
@ -289,15 +338,21 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
return res, nil
}
formattedAmount := fmt.Sprintf("%f", inputAmount)
// Format the amount to 2 decimal places
formattedAmount, err := store.TruncateDecimalString(fmt.Sprintf("%f", inputAmount), 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)
finalAmountStr, err := store.ParseAndScaleAmount(formattedAmount, swapToVoucher.TokenDecimals)
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)
r, err := h.accountService.GetCreditSendReverseQuote(ctx, string(activePoolAddress), mpesaWithdrawalVoucher.TokenAddress, swapToVoucher.TokenAddress, finalAmountStr)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
@ -323,13 +378,13 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
}
// covert for display
quoteInputStr := store.ScaleDownBalance(sendInputAmount, swapData.ActiveSwapFromDecimal)
quoteInputStr := store.ScaleDownBalance(sendInputAmount, mpesaWithdrawalVoucher.TokenDecimals)
// Format the quoteInputStr amount to 2 decimal places
qouteInputAmount, _ := store.TruncateDecimalString(quoteInputStr, 2)
res.Content = l.Get(
"You are sending %s %s in order to receive ~ %s ksh",
qouteInputAmount, swapData.ActiveSwapFromSym, inputStr,
qouteInputAmount, mpesaWithdrawalVoucher.TokenSymbol, inputStr,
)
return res, nil
@ -355,13 +410,15 @@ func (h *MenuHandlers) InitiateGetMpesa(ctx context.Context, sym string, input [
mpesaAddress := config.DefaultMpesaAddress()
swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId)
transactionType, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE)
if err != nil {
return res, err
}
transactionType, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE)
// get the selected voucher
mpesaWithdrawalVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
@ -372,12 +429,12 @@ func (h *MenuHandlers) InitiateGetMpesa(ctx context.Context, sym string, input [
return res, err
}
finalAmountStr, err := store.ParseAndScaleAmount(data.Amount, data.ActiveDecimal)
finalAmountStr, err := store.ParseAndScaleAmount(data.Amount, mpesaWithdrawalVoucher.TokenDecimals)
if err != nil {
return res, err
}
tokenTransfer, err := h.accountService.TokenTransfer(ctx, finalAmountStr, data.PublicKey, mpesaAddress, data.ActiveAddress)
tokenTransfer, err := h.accountService.TokenTransfer(ctx, finalAmountStr, data.PublicKey, mpesaAddress, mpesaWithdrawalVoucher.TokenAddress)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
@ -387,11 +444,28 @@ func (h *MenuHandlers) InitiateGetMpesa(ctx context.Context, sym string, input [
logg.InfoCtxf(ctx, "TokenTransfer normal", "trackingId", tokenTransfer.TrackingId)
res.Content = l.Get("Your request has been sent. You will receive ~ %s ksh", data.TemporaryValue)
res.Content = l.Get("Your request has been sent. Please await confirmation")
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry", "key", storedb.DATA_PUBLIC_KEY, "error", err)
return res, err
}
swapToVoucher, err := store.ReadSwapToVoucher(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on ReadSwapFromVoucher", "error", err)
return res, err
}
// Resolve active pool
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
swapAmount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_AMOUNT)
@ -403,7 +477,7 @@ func (h *MenuHandlers) InitiateGetMpesa(ctx context.Context, sym string, input [
swapAmountStr := string(swapAmount)
// Call the poolSwap API
poolSwap, err := h.accountService.PoolSwap(ctx, swapAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress)
poolSwap, err := h.accountService.PoolSwap(ctx, swapAmountStr, string(publicKey), mpesaWithdrawalVoucher.TokenAddress, string(activePoolAddress), swapToVoucher.TokenAddress)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
@ -417,18 +491,13 @@ func (h *MenuHandlers) InitiateGetMpesa(ctx context.Context, sym string, input [
// TODO: remove this temporary time delay and replace with a swap and send endpoint
time.Sleep(1 * time.Second)
finalKshStr, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if err != nil {
return res, err
}
amount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_AMOUNT)
if err != nil {
return res, err
}
// Initiate a send to mpesa after the swap
tokenTransfer, err := h.accountService.TokenTransfer(ctx, string(amount), swapData.PublicKey, mpesaAddress, swapData.ActiveSwapToAddress)
tokenTransfer, err := h.accountService.TokenTransfer(ctx, string(amount), string(publicKey), mpesaAddress, swapToVoucher.TokenAddress)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
@ -438,7 +507,7 @@ func (h *MenuHandlers) InitiateGetMpesa(ctx context.Context, sym string, input [
logg.InfoCtxf(ctx, "final TokenTransfer after swap", "trackingId", tokenTransfer.TrackingId)
res.Content = l.Get("Your request has been sent. You will receive ~ %s ksh", finalKshStr)
res.Content = l.Get("Your request has been sent. Please await confirmation")
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
@ -466,7 +535,7 @@ func (h *MenuHandlers) SendMpesaMinLimit(ctx context.Context, sym string, input
kshFormatted, _ := store.TruncateDecimalString(ksh, 0)
res.Content = l.Get(
"Enter the amount of Mpesa to send: (Minimum %s Ksh)\n",
"Enter the amount of credit to deposit: (Minimum %s Ksh)\n",
kshFormatted,
)
@ -535,9 +604,11 @@ func (h *MenuHandlers) SendMpesaPreview(ctx context.Context, sym string, input [
estimateStr := fmt.Sprintf("%f", estimateValue)
estimateFormatted, _ := store.TruncateDecimalString(estimateStr, 2)
defaultAsset := config.DefaultMpesaAsset()
res.Content = l.Get(
"You will get a prompt for your M-Pesa PIN shortly to send %s ksh and receive ~ %s cUSD",
inputStr, estimateFormatted,
"You will get a prompt for your Mpesa PIN shortly to send %s ksh and receive ~ %s %s",
inputStr, estimateFormatted, defaultAsset,
)
return res, nil

View File

@ -0,0 +1,329 @@
package application
import (
"context"
"fmt"
"strconv"
"strings"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/sarafu-vise/config"
"git.grassecon.net/grassrootseconomics/sarafu-vise/store"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
"gopkg.in/leonelquinteros/gotext.v1"
)
// CalculateMaxPayDebt calculates the max debt removal based on the selected voucher
func (h *MenuHandlers) CalculateMaxPayDebt(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")
}
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_low_swap_amount, _ := h.flagManager.GetFlag("flag_low_swap_amount")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
inputStr := string(input)
if inputStr == "0" || inputStr == "99" || inputStr == "88" || inputStr == "98" {
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error)
return res, nil
}
userStore := h.userdataStore
// Fetch session data
_, _, activeSym, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId)
if err != nil {
return res, nil
}
// Resolve active pool
activePoolAddress, activePoolSymbol, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error)
return res, err
}
// get the voucher data based on the input
metadata, err := store.GetVoucherData(ctx, userStore, sessionId, inputStr)
if err != nil {
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error)
return res, fmt.Errorf("failed to retrieve swap to voucher data: %v", err)
}
if metadata == nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
return res, nil
}
// Get the max swap limit with the selected voucher
r, err := h.accountService.GetSwapFromTokenMaxLimit(ctx, string(activePoolAddress), metadata.TokenAddress, string(activeAddress), string(publicKey))
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = "0"
logg.ErrorCtxf(ctx, "failed on GetSwapFromTokenMaxLimit", "error", err)
return res, nil
}
maxLimit := r.Max
metadata.Balance = maxLimit
// Store the active swap from data
if err := store.UpdateSwapFromVoucherData(ctx, userStore, sessionId, metadata); err != nil {
logg.ErrorCtxf(ctx, "failed on UpdateSwapFromVoucherData", "error", err)
return res, err
}
// Scale down the amount
maxAmountStr := store.ScaleDownBalance(maxLimit, metadata.TokenDecimals)
if err != nil {
return res, err
}
maxAmountFloat, err := strconv.ParseFloat(maxAmountStr, 64)
if err != nil {
logg.ErrorCtxf(ctx, "failed to parse maxAmountStr as float", "value", maxAmountStr, "error", err)
return res, err
}
// Format to 2 decimal places
maxStr, _ := store.TruncateDecimalString(string(maxAmountStr), 2)
if maxAmountFloat < 0.1 {
// return with low amount flag
res.Content = maxStr
res.FlagSet = append(res.FlagSet, flag_low_swap_amount)
return res, nil
}
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, []byte(maxStr))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write swap max amount entry with", "key", storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, "value", maxStr, "error", err)
return res, err
}
// Do a pool quote to get the max AT that can be removed (gotten)
// if we swapped the max of the FT
// call the API to get the quote
qoute, err := h.accountService.GetPoolSwapQuote(ctx, maxLimit, string(publicKey), metadata.TokenAddress, string(activePoolAddress), string(activeAddress))
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil
}
// Scale down the quoted amount
quoteAmountStr := store.ScaleDownBalance(qoute.OutValue, string(activeDecimal))
// Format to 2 decimal places
quoteStr, _ := store.TruncateDecimalString(string(quoteAmountStr), 2)
res.Content = l.Get(
"You can remove a max of %s %s from '%s' pool\nEnter amount of %s:(Max: %s)",
quoteStr,
string(activeSym),
string(activePoolSymbol),
metadata.TokenSymbol,
maxStr,
)
res.FlagReset = append(res.FlagReset, flag_low_swap_amount, flag_api_call_error)
return res, nil
}
// ConfirmDebtRemoval displays the debt preview for a confirmation
func (h *MenuHandlers) ConfirmDebtRemoval(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
inputStr := string(input)
if inputStr == "0" {
return res, nil
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
// Fetch session data
_, _, activeSym, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId)
if err != nil {
return res, nil
}
payDebtVoucher, err := store.ReadSwapFromVoucher(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on ReadSwapFromVoucher", "error", err)
return res, err
}
// Resolve active pool
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
swapMaxAmount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read swapMaxAmount entry with", "key", storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, "error", err)
return res, err
}
maxValue, err := strconv.ParseFloat(string(swapMaxAmount), 64)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to convert the swapMaxAmount to a float", "error", err)
return res, err
}
inputAmount, err := strconv.ParseFloat(inputStr, 64)
if err != nil || inputAmount > maxValue || inputAmount < 0.1 {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
var finalAmountStr string
if inputStr == string(swapMaxAmount) {
finalAmountStr = string(payDebtVoucher.Balance)
} else {
finalAmountStr, err = store.ParseAndScaleAmount(inputStr, payDebtVoucher.TokenDecimals)
if err != nil {
return res, err
}
}
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_AMOUNT, []byte(finalAmountStr))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write swap amount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "value", finalAmountStr, "error", err)
return res, err
}
// call the API to get the quote
r, err := h.accountService.GetPoolSwapQuote(ctx, finalAmountStr, string(publicKey), payDebtVoucher.TokenAddress, string(activePoolAddress), string(activeAddress))
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil
}
// Scale down the quoted amount (for the AT)
quoteAmountStr := store.ScaleDownBalance(r.OutValue, string(activeDecimal))
// Format to 2 decimal places
qouteStr, _ := store.TruncateDecimalString(string(quoteAmountStr), 2)
// store the quote in the temporary value key
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(qouteStr))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write swap max amount entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", qouteStr, "error", err)
return res, err
}
res.Content = l.Get(
"Please confirm that you will use %s %s to remove your debt of %s %s\nEnter your PIN:",
inputStr, payDebtVoucher.TokenSymbol, qouteStr, string(activeSym),
)
return res, nil
}
// InitiatePayDebt calls the poolSwap to swap the token for the active voucher.
func (h *MenuHandlers) InitiatePayDebt(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")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
// Fetch session data
_, _, activeSym, activeAddress, publicKey, _, err := h.getSessionData(ctx, sessionId)
if err != nil {
return res, nil
}
// Resolve active pool
activePoolAddress, activePoolSymbol, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
payDebtVoucher, err := store.ReadSwapFromVoucher(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on ReadSwapFromVoucher", "error", err)
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
}
debtQuotedAmount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read debtQuotedAmount entry with", "key", storedb.DATA_TEMPORARY_VALUE, "error", err)
return res, err
}
swapAmountStr := string(swapAmount)
// Call the poolSwap API
r, err := h.accountService.PoolSwap(ctx, swapAmountStr, string(publicKey), payDebtVoucher.TokenAddress, string(activePoolAddress), string(activeAddress))
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil
}
trackingId := r.TrackingId
logg.InfoCtxf(ctx, "poolSwap", "trackingId", trackingId)
res.Content = l.Get(
"Your request has been sent. You will receive an SMS when your debt of %s %s has been removed from %s.",
string(debtQuotedAmount),
string(activeSym),
activePoolSymbol,
)
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
func isStableVoucher(tokenAddress string) bool {
addr := strings.TrimSpace(tokenAddress)
for _, stable := range config.StableVoucherAddresses() {
if addr == stable {
return true
}
}
return false
}

View File

@ -0,0 +1,231 @@
package application
import (
"context"
"fmt"
"strconv"
"strings"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/sarafu-vise/store"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
"gopkg.in/leonelquinteros/gotext.v1"
)
// GetOrderedVouchers returns a list of ordered vouchers with stables at the top
func (h *MenuHandlers) GetOrderedVouchers(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")
}
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
// Read ordered vouchers from the store
voucherData, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ORDERED_VOUCHER_SYMBOLS)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read stable voucherData entires with", "key", storedb.DATA_ORDERED_VOUCHER_SYMBOLS, "error", err)
return res, err
}
if len(voucherData) == 0 {
return res, nil
}
voucherBalances, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ORDERED_VOUCHER_BALANCES)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read stable voucherData entires with", "key", storedb.DATA_ORDERED_VOUCHER_BALANCES, "error", err)
return res, err
}
formattedVoucherList := store.FormatVoucherList(ctx, string(voucherData), string(voucherBalances))
finalOutput := strings.Join(formattedVoucherList, "\n")
res.Content = l.Get("Select number or symbol from your vouchers:\n%s", finalOutput)
return res, nil
}
// PoolDepositMaxAmount returns the balance of the selected voucher
func (h *MenuHandlers) PoolDepositMaxAmount(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")
}
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
inputStr := string(input)
if inputStr == "0" || inputStr == "99" || inputStr == "88" || inputStr == "98" {
return res, nil
}
userStore := h.userdataStore
metadata, err := store.GetOrderedVoucherData(ctx, userStore, sessionId, inputStr)
if err != nil {
return res, fmt.Errorf("failed to retrieve swap to voucher data: %v", err)
}
if metadata == nil {
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher)
return res, nil
}
// Store the pool deposit voucher data
if err := store.StoreTransactionVoucher(ctx, h.userdataStore, sessionId, metadata); err != nil {
logg.ErrorCtxf(ctx, "failed on StoreTransactionVoucher", "error", err)
return res, err
}
// Format the balance amount to 2 decimal places
formattedBalance, _ := store.TruncateDecimalString(string(metadata.Balance), 2)
res.Content = l.Get("Maximum amount: %s %s\nEnter amount:", formattedBalance, metadata.TokenSymbol)
return res, nil
}
// ConfirmPoolDeposit displays the pool deposit preview for a PIN confirmation
func (h *MenuHandlers) ConfirmPoolDeposit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
inputStr := string(input)
if inputStr == "0" {
return res, nil
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
res.FlagReset = append(res.FlagReset, flag_invalid_amount)
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
poolDepositVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
maxValue, err := strconv.ParseFloat(poolDepositVoucher.Balance, 64)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to convert the swapMaxAmount to a float", "error", err)
return res, err
}
inputAmount, err := strconv.ParseFloat(inputStr, 64)
if err != nil || inputAmount > maxValue || inputAmount < 0.1 {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(inputStr))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write pool deposit amount entry with", "key", storedb.DATA_AMOUNT, "value", inputStr, "error", err)
return res, err
}
// Resolve active pool
_, activePoolSymbol, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
res.Content = l.Get(
"You will deposit %s %s into %s\n",
inputStr, poolDepositVoucher.TokenSymbol, activePoolSymbol,
)
return res, nil
}
// InitiatePoolDeposit calls the pool deposit API
func (h *MenuHandlers) InitiatePoolDeposit(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")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
// Resolve active pool
activePoolAddress, activePoolSymbol, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on resolveActivePoolDetails", "error", err)
return res, err
}
poolDepositVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry", "key", storedb.DATA_PUBLIC_KEY, "error", err)
return res, err
}
amount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_AMOUNT)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read amount entry", "key", storedb.DATA_AMOUNT, "error", err)
return res, err
}
finalAmountStr, err := store.ParseAndScaleAmount(string(amount), poolDepositVoucher.TokenDecimals)
if err != nil {
logg.ErrorCtxf(ctx, "failed on ParseAndScaleAmount", "error", err)
return res, err
}
// Call pool deposit API
r, err := h.accountService.PoolDeposit(ctx, finalAmountStr, string(publicKey), string(activePoolAddress), poolDepositVoucher.TokenAddress)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on pool deposit", "error", err)
return res, nil
}
trackingId := r.TrackingId
logg.InfoCtxf(ctx, "Pool deposit", "trackingId", trackingId)
res.Content = l.Get(
"Your request has been sent. You will receive an SMS when %s %s has been deposited into %s.",
string(amount),
poolDepositVoucher.TokenSymbol,
activePoolSymbol,
)
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}

View File

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

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
"time"
@ -49,10 +48,10 @@ func (h *MenuHandlers) ValidateRecipient(ctx context.Context, sym string, input
return res, nil
}
// save the recipient as the temporaryRecipient
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(recipient))
// save the recipient under recipient input
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_INPUT, []byte(recipient))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryRecipient entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", recipient, "error", err)
logg.ErrorCtxf(ctx, "failed to write recipient input entry with", "key", storedb.DATA_RECIPIENT_INPUT, "value", recipient, "error", err)
return res, err
}
@ -204,11 +203,11 @@ func (h *MenuHandlers) determineAndSaveTransactionType(
publicKey []byte,
recipientPhoneNumber []byte,
) error {
store := h.userdataStore
userStore := h.userdataStore
txType := "swap"
// Read sender's active address
senderActiveAddress, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
senderActiveAddress, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to read sender active address", "error", err)
return err
@ -216,7 +215,7 @@ func (h *MenuHandlers) determineAndSaveTransactionType(
var recipientActiveAddress []byte
if recipientPhoneNumber != nil {
recipientActiveAddress, _ = store.ReadEntry(ctx, string(recipientPhoneNumber), storedb.DATA_ACTIVE_ADDRESS)
recipientActiveAddress, _ = userStore.ReadEntry(ctx, string(recipientPhoneNumber), storedb.DATA_ACTIVE_ADDRESS)
}
// recipient has no active token → normal transaction
@ -228,17 +227,34 @@ func (h *MenuHandlers) determineAndSaveTransactionType(
}
// Save the transaction type
if err := store.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 {
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 {
if err := userStore.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
}
// fetch data for use (to_voucher data)
recipientActiveSym, recipientActiveAddress, recipientActiveDecimal, err := h.getRecipientData(ctx, string(recipientPhoneNumber))
if err != nil {
return err
}
swapMetadata := &dataserviceapi.TokenHoldings{
TokenAddress: string(recipientActiveAddress),
TokenSymbol: string(recipientActiveSym),
TokenDecimals: string(recipientActiveDecimal),
}
// Store the active swap_to data
if err := store.UpdateSwapToVoucherData(ctx, userStore, sessionId, swapMetadata); err != nil {
logg.ErrorCtxf(ctx, "failed on UpdateSwapToVoucherData", "error", err)
return err
}
} else {
logg.InfoCtxf(ctx, "No recipient phone number found for public key", "publicKey", string(publicKey))
}
@ -310,6 +326,7 @@ func (h *MenuHandlers) ResetTransactionAmount(ctx context.Context, sym string, i
}
// MaxAmount checks the transaction type to determine the displayed max amount.
// Checks whether the user selected a custom voucher
// 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.
@ -335,9 +352,41 @@ func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte)
return res, err
}
customVoucherSelection, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_TRANSACTION_CUSTOM_VOUCHER_STATE)
if err == nil {
customVoucherValue, _ := strconv.ParseUint(string(customVoucherSelection), 0, 64)
if customVoucherValue == 1 {
// use the custom voucher
customTransactionVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
activeSym = []byte(customTransactionVoucher.TokenSymbol)
activeBal = []byte(customTransactionVoucher.Balance)
activeDecimal = []byte(customTransactionVoucher.TokenDecimals)
activeAddress = []byte(customTransactionVoucher.TokenAddress)
}
}
// Format the active balance amount to 2 decimal places
formattedBalance, _ := store.TruncateDecimalString(string(activeBal), 2)
// confirm the transaction type
swapToVoucher, err := store.ReadSwapToVoucher(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on ReadSwapFromVoucher", "error", err)
return res, err
}
if string(swapToVoucher.TokenAddress) == string(activeAddress) {
// recipient has active token same as selected token → normal transaction
transactionType = []byte("normal")
} else {
transactionType = []byte("swap")
}
// If normal transaction return balance
if string(transactionType) == "normal" {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
@ -346,7 +395,7 @@ func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte)
}
// Resolve active pool address
activePoolAddress, err := h.resolveActivePoolAddress(ctx, sessionId)
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
@ -365,12 +414,15 @@ func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte)
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)
if err != nil {
// invalid state
return res, err
if err != nil || !phone.IsValidPhoneNumber(string(recipientPhoneNumber)) {
// revert to normal transaction
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))
if err != nil {
return res, err
@ -480,22 +532,53 @@ func (h *MenuHandlers) getRecipientData(ctx context.Context, sessionId string) (
return
}
func (h *MenuHandlers) resolveActivePoolAddress(ctx context.Context, sessionId string) ([]byte, error) {
func (h *MenuHandlers) resolveActivePoolDetails(ctx context.Context, sessionId string) (defaultPoolAddress, defaultPoolSymbol []byte, err error) {
store := h.userdataStore
addr, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS)
if err == nil {
return addr, nil
// Try read address
defaultPoolAddress, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS)
if err != nil && !db.IsNotFound(err) {
logg.ErrorCtxf(ctx, "failed to read active pool address", "error", err)
return nil, nil, err
}
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
// Try read symbol
defaultPoolSymbol, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_SYM)
if err != nil && !db.IsNotFound(err) {
logg.ErrorCtxf(ctx, "failed to read active pool name", "error", err)
return nil, nil, err
}
logg.ErrorCtxf(ctx, "failed to read active pool address", "error", err)
return nil, err
// If both exist, return them
if defaultPoolAddress != nil && defaultPoolSymbol != nil {
return defaultPoolAddress, defaultPoolSymbol, nil
}
// Fallback to config defaults
defaultPoolAddress = []byte(config.DefaultPoolAddress())
defaultPoolSymbol = []byte(config.DefaultPoolSymbol())
if err := store.WriteEntry(
ctx,
sessionId,
storedb.DATA_ACTIVE_POOL_ADDRESS,
defaultPoolAddress,
); err != nil {
logg.ErrorCtxf(ctx, "failed to write default pool address", "error", err)
return nil, nil, err
}
if err := store.WriteEntry(
ctx,
sessionId,
storedb.DATA_ACTIVE_POOL_SYM,
defaultPoolSymbol,
); err != nil {
logg.ErrorCtxf(ctx, "failed to write default pool symbol", "error", err)
return nil, nil, err
}
return defaultPoolAddress, defaultPoolSymbol, nil
}
func (h *MenuHandlers) calculateSendCreditLimits(ctx context.Context, poolAddress, fromAddress, toAddress, publicKey, fromDecimal, toDecimal []byte) (string, string, error) {
@ -530,9 +613,14 @@ func (h *MenuHandlers) ValidateAmount(ctx context.Context, sym string, input []b
return res, fmt.Errorf("missing session")
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
userStore := h.userdataStore
var balanceValue float64
inputStr := string(input)
if inputStr == "0" {
res.FlagReset = append(res.FlagReset, flag_invalid_amount)
return res, nil
}
userStore := h.userdataStore
// retrieve the active balance
activeBal, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL)
@ -540,32 +628,40 @@ func (h *MenuHandlers) ValidateAmount(ctx context.Context, sym string, input []b
logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", storedb.DATA_ACTIVE_BAL, "error", err)
return res, err
}
balanceValue, err = strconv.ParseFloat(string(activeBal), 64)
customVoucherSelection, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_TRANSACTION_CUSTOM_VOUCHER_STATE)
if err == nil {
customVoucherValue, _ := strconv.ParseUint(string(customVoucherSelection), 0, 64)
if customVoucherValue == 1 {
// use the custom voucher
customTransactionVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
activeBal = []byte(customTransactionVoucher.Balance)
}
}
maxValue, err := strconv.ParseFloat(string(activeBal), 64)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to convert the activeBal to a float", "error", err)
return res, err
}
// Extract numeric part from the input amount
amountStr := strings.TrimSpace(string(input))
inputAmount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
inputAmount, err := strconv.ParseFloat(inputStr, 64)
if err != nil || inputAmount > maxValue || inputAmount < 0.1 {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr
return res, nil
}
if inputAmount > balanceValue {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr
res.Content = inputStr
return res, nil
}
// Format the amount to 2 decimal places before saving (truncated)
formattedAmount, err := store.TruncateDecimalString(amountStr, 2)
formattedAmount, err := store.TruncateDecimalString(inputStr, 2)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr
res.Content = inputStr
return res, nil
}
@ -637,8 +733,62 @@ func (h *MenuHandlers) GetAmount(ctx context.Context, sym string, input []byte)
return res, nil
}
// InitiateTransaction calls the TokenTransfer and returns a confirmation based on the result.
func (h *MenuHandlers) InitiateTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
// NormalTransactionPreview displays the token transfer preview awaiting authorization
func (h *MenuHandlers) NormalTransactionPreview(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
}
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
data, err := store.ReadTransactionData(ctx, h.userdataStore, sessionId)
if err != nil {
return res, err
}
customVoucherSelection, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_TRANSACTION_CUSTOM_VOUCHER_STATE)
if err == nil {
customVoucherValue, _ := strconv.ParseUint(string(customVoucherSelection), 0, 64)
if customVoucherValue == 1 {
// use the custom voucher
customTransactionVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
data.ActiveSym = customTransactionVoucher.TokenSymbol
data.ActiveDecimal = customTransactionVoucher.TokenDecimals
data.ActiveAddress = customTransactionVoucher.TokenAddress
}
}
res.Content = l.Get(
"%s will receive %s %s from %s",
data.RecipientInput,
data.Amount,
data.ActiveSym,
sessionId,
)
return res, nil
}
// InitiateNormalTransaction calls the TokenTransfer and returns a confirmation based on the result.
// used for non-swap transactions
func (h *MenuHandlers) InitiateNormalTransaction(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
@ -648,6 +798,8 @@ func (h *MenuHandlers) InitiateTransaction(ctx context.Context, sym string, inpu
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
userStore := h.userdataStore
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
@ -657,6 +809,23 @@ func (h *MenuHandlers) InitiateTransaction(ctx context.Context, sym string, inpu
return res, err
}
customVoucherSelection, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_TRANSACTION_CUSTOM_VOUCHER_STATE)
if err == nil {
customVoucherValue, _ := strconv.ParseUint(string(customVoucherSelection), 0, 64)
if customVoucherValue == 1 {
// use the custom voucher
customTransactionVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
data.ActiveSym = customTransactionVoucher.TokenSymbol
data.ActiveDecimal = customTransactionVoucher.TokenDecimals
data.ActiveAddress = customTransactionVoucher.TokenAddress
}
}
finalAmountStr, err := store.ParseAndScaleAmount(data.Amount, data.ActiveDecimal)
if err != nil {
return res, err
@ -688,7 +857,7 @@ func (h *MenuHandlers) InitiateTransaction(ctx context.Context, sym string, inpu
res.Content = l.Get(
"Your request has been sent. %s will receive %s %s from %s.",
data.TemporaryValue,
data.RecipientInput,
data.Amount,
data.ActiveSym,
sessionId,
@ -720,19 +889,40 @@ func (h *MenuHandlers) TransactionSwapPreview(ctx context.Context, sym string, i
userStore := h.userdataStore
recipientPhoneNumber, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER)
// the initial recipient that the sender input
recipientInput, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_INPUT)
if err != nil {
// invalid state
return res, err
}
swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId)
// Resolve active pool
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
// get the selected voucher
selectedVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
swapToVoucher, err := store.ReadSwapToVoucher(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on ReadSwapFromVoucher", "error", err)
return res, err
}
swapMaxAmount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read swapMaxAmount entry with", "key", storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, "error", err)
return res, err
}
// use the stored max RAT
maxRATValue, err := strconv.ParseFloat(swapData.ActiveSwapMaxAmount, 64)
maxRATValue, err := strconv.ParseFloat(string(swapMaxAmount), 64)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to convert the swapMaxAmount to a float", "error", err)
return res, err
@ -753,24 +943,13 @@ func (h *MenuHandlers) TransactionSwapPreview(ctx context.Context, sym string, i
return res, nil
}
finalAmountStr, err := store.ParseAndScaleAmount(formattedAmount, swapData.ActiveSwapToDecimal)
finalAmountStr, err := store.ParseAndScaleAmount(formattedAmount, swapToVoucher.TokenDecimals)
if err != nil {
return res, err
}
// multiply by 1.015 (i.e. * 1015 / 1000)
amountInt, ok := new(big.Int).SetString(finalAmountStr, 10)
if !ok {
return res, fmt.Errorf("invalid amount: %s", finalAmountStr)
}
amountInt.Mul(amountInt, big.NewInt(1015))
amountInt.Div(amountInt, big.NewInt(1000))
scaledFinalAmountStr := amountInt.String()
// call the credit send API to get the reverse quote
r, err := h.accountService.GetCreditSendReverseQuote(ctx, swapData.ActivePoolAddress, swapData.ActiveSwapFromAddress, swapData.ActiveSwapToAddress, scaledFinalAmountStr)
r, err := h.accountService.GetCreditSendReverseQuote(ctx, string(activePoolAddress), selectedVoucher.TokenAddress, swapToVoucher.TokenAddress, finalAmountStr)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
@ -779,22 +958,15 @@ func (h *MenuHandlers) TransactionSwapPreview(ctx context.Context, sym string, i
return res, nil
}
sendInputAmount := r.InputAmount // amount of SAT that should be swapped
sendOutputAmount := r.OutputAmount // amount of RAT that will be received
sendInputAmount := r.InputAmount // amount of SAT that should be swapped
// store the finalAmountStr as the final amount (that will be sent after the swap)
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(finalAmountStr))
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
}
// 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(inputStr))
if err != nil {
@ -811,7 +983,9 @@ func (h *MenuHandlers) TransactionSwapPreview(ctx context.Context, sym string, i
res.Content = l.Get(
"%s will receive %s %s",
string(recipientPhoneNumber), inputStr, swapData.ActiveSwapToSym,
string(recipientInput),
inputStr,
swapToVoucher.TokenSymbol,
)
return res, nil
@ -835,11 +1009,37 @@ func (h *MenuHandlers) TransactionInitiateSwap(ctx context.Context, sym string,
userStore := h.userdataStore
swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId)
// Resolve active pool
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
// get the selected voucher
selectedVoucher, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry", "key", storedb.DATA_PUBLIC_KEY, "error", err)
return res, err
}
swapToVoucher, err := store.ReadSwapToVoucher(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on ReadSwapFromVoucher", "error", err)
return res, err
}
quotedAmount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read quotedAmount entry with", "key", storedb.DATA_TEMPORARY_VALUE, "error", err)
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)
@ -849,7 +1049,7 @@ func (h *MenuHandlers) TransactionInitiateSwap(ctx context.Context, sym string,
swapAmountStr := string(swapAmount)
// Call the poolSwap API
poolSwap, err := h.accountService.PoolSwap(ctx, swapAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress)
poolSwap, err := h.accountService.PoolSwap(ctx, swapAmountStr, string(publicKey), selectedVoucher.TokenAddress, string(activePoolAddress), swapToVoucher.TokenAddress)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
@ -870,7 +1070,7 @@ func (h *MenuHandlers) TransactionInitiateSwap(ctx context.Context, sym string,
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)
recipientInput, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_INPUT)
if err != nil {
// invalid state
return res, err
@ -884,7 +1084,7 @@ func (h *MenuHandlers) TransactionInitiateSwap(ctx context.Context, sym string,
}
// Call TokenTransfer with the expected swap amount
tokenTransfer, err := h.accountService.TokenTransfer(ctx, string(amount), swapData.PublicKey, string(recipientPublicKey), swapData.ActiveSwapToAddress)
tokenTransfer, err := h.accountService.TokenTransfer(ctx, string(amount), string(publicKey), string(recipientPublicKey), swapToVoucher.TokenAddress)
if err != nil {
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_call_error)
@ -898,9 +1098,9 @@ func (h *MenuHandlers) TransactionInitiateSwap(ctx context.Context, sym string,
res.Content = l.Get(
"Your request has been sent. %s will receive %s %s from %s.",
string(recipientPhoneNumber),
swapData.TemporaryValue,
swapData.ActiveSwapToSym,
string(recipientInput),
string(quotedAmount),
swapToVoucher.TokenSymbol,
sessionId,
)
@ -913,10 +1113,11 @@ func (h *MenuHandlers) ClearTransactionTypeFlag(ctx context.Context, sym string,
var res resource.Result
flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction")
flag_multiple_voucher, _ := h.flagManager.GetFlag("flag_multiple_voucher")
inputStr := string(input)
if inputStr == "0" {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
res.FlagReset = append(res.FlagReset, flag_swap_transaction, flag_multiple_voucher)
return res, nil
}

View File

@ -452,7 +452,7 @@ func TestGetAmount(t *testing.T) {
assert.Equal(t, formattedAmount, res.Content)
}
func TestInitiateTransaction(t *testing.T) {
func TestInitiateNormalTransaction(t *testing.T) {
sessionId := "254712345678"
ctx, store := InitializeTestStore(t)
ctx = context.WithValue(ctx, "SessionId", sessionId)
@ -538,7 +538,7 @@ func TestInitiateTransaction(t *testing.T) {
mockAccountService.On("TokenTransfer").Return(tt.TransferResponse, nil)
// Call the method under test
res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", []byte(""))
res, _ := h.InitiateNormalTransaction(ctx, "transaction_reset_amount", []byte(""))
// Assert that no errors occurred
assert.NoError(t, err)

View File

@ -3,10 +3,12 @@ package application
import (
"context"
"fmt"
"sort"
"strings"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/sarafu-vise/config"
"git.grassecon.net/grassrootseconomics/sarafu-vise/store"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
@ -15,8 +17,9 @@ import (
// 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.
// 2. Stores list of vouchers
// 3. updates the balance of the active voucher
// 2. Stores list of filtered ordered vouchers (exclude the active voucher)
// 3. Stores list of ordered vouchers (all vouchers)
// 4. updates the balance of the active voucher
func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
userStore := h.userdataStore
@ -29,6 +32,7 @@ func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []b
flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_multiple_voucher, _ := h.flagManager.GetFlag("flag_multiple_voucher")
publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
@ -69,6 +73,13 @@ func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []b
return res, nil
}
// only set the flag if the user has a single voucher
if len(vouchersResp) == 1 {
res.FlagReset = append(res.FlagReset, flag_multiple_voucher)
} else {
res.FlagSet = append(res.FlagSet, flag_multiple_voucher)
}
res.FlagReset = append(res.FlagReset, flag_no_active_voucher)
// add a variable to filter out the active voucher
@ -144,7 +155,36 @@ func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []b
}
}
// Filter out the active voucher from vouchersResp
// Build stable voucher priority (lower index = higher priority)
stablePriority := make(map[string]int)
stableAddresses := config.StableVoucherAddresses()
for i, addr := range stableAddresses {
stablePriority[addr] = i
}
// Helper: order vouchers (stable first, priority-based)
orderVouchers := func(vouchers []dataserviceapi.TokenHoldings) []dataserviceapi.TokenHoldings {
stable := make([]dataserviceapi.TokenHoldings, 0)
nonStable := make([]dataserviceapi.TokenHoldings, 0)
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(vouchersResp))
for _, v := range vouchersResp {
if v.TokenSymbol != activeSymStr {
@ -152,8 +192,11 @@ func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []b
}
}
// Store all voucher data (excluding the current active voucher)
data := store.ProcessVouchers(filteredVouchers)
// Order remaining vouchers
orderedFilteredVouchers := orderVouchers(filteredVouchers)
// Process & store
data := store.ProcessVouchers(orderedFilteredVouchers)
dataMap := map[storedb.DataTyp]string{
storedb.DATA_VOUCHER_SYMBOLS: data.Symbols,
@ -164,6 +207,26 @@ func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []b
// Write data entries
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)
}
}
// Order all vouchers
orderedVouchers := orderVouchers(vouchersResp)
// Process ALL vouchers (stable first)
orderedVoucherData := store.ProcessVouchers(orderedVouchers)
orderedVoucherDataMap := map[storedb.DataTyp]string{
storedb.DATA_ORDERED_VOUCHER_SYMBOLS: orderedVoucherData.Symbols,
storedb.DATA_ORDERED_VOUCHER_BALANCES: orderedVoucherData.Balances,
storedb.DATA_ORDERED_VOUCHER_DECIMALS: orderedVoucherData.Decimals,
storedb.DATA_ORDERED_VOUCHER_ADDRESSES: orderedVoucherData.Addresses,
}
// Write data entries
for key, value := range orderedVoucherDataMap {
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
@ -174,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.
// 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) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
@ -181,8 +245,18 @@ func (h *MenuHandlers) GetVoucherList(ctx context.Context, sym string, input []b
return res, fmt.Errorf("missing session")
}
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
// Fetch session data
_, _, activeSym, _, _, _, err := h.getSessionData(ctx, sessionId)
if err != nil {
return res, nil
}
// Read vouchers from the store
voucherData, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_VOUCHER_SYMBOLS)
logg.InfoCtxf(ctx, "reading voucherData in GetVoucherList", "sessionId", sessionId, "key", storedb.DATA_VOUCHER_SYMBOLS, "voucherData", voucherData)
@ -191,6 +265,16 @@ func (h *MenuHandlers) GetVoucherList(ctx context.Context, sym string, input []b
return res, err
}
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))
}
return res, nil
}
voucherBalances, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_VOUCHER_BALANCES)
logg.InfoCtxf(ctx, "reading voucherBalances in GetVoucherList", "sessionId", sessionId, "key", storedb.DATA_VOUCHER_BALANCES, "voucherBalances", voucherBalances)
if err != nil {
@ -203,7 +287,7 @@ func (h *MenuHandlers) GetVoucherList(ctx context.Context, sym string, input []b
logg.InfoCtxf(ctx, "final output for GetVoucherList", "sessionId", sessionId, "finalOutput", finalOutput)
res.Content = finalOutput
res.Content = l.Get("Select number or symbol from your vouchers:\n%s", finalOutput)
return res, nil
}
@ -239,8 +323,8 @@ func (h *MenuHandlers) ViewVoucher(ctx context.Context, sym string, input []byte
return res, nil
}
if err := store.StoreTemporaryVoucher(ctx, h.userdataStore, sessionId, metadata); err != nil {
logg.ErrorCtxf(ctx, "failed on StoreTemporaryVoucher", "error", err)
if err := store.StoreTransactionVoucher(ctx, h.userdataStore, sessionId, metadata); err != nil {
logg.ErrorCtxf(ctx, "failed on StoreTransactionVoucher", "error", err)
return res, err
}
@ -268,9 +352,9 @@ func (h *MenuHandlers) SetVoucher(ctx context.Context, sym string, input []byte)
}
// Get temporary data
tempData, err := store.GetTemporaryVoucherData(ctx, h.userdataStore, sessionId)
tempData, err := store.GetTransactionVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTemporaryVoucherData", "error", err)
logg.ErrorCtxf(ctx, "failed on GetTransactionVoucherData", "error", err)
return res, err
}
@ -318,3 +402,50 @@ func (h *MenuHandlers) GetVoucherDetails(ctx context.Context, sym string, input
return res, nil
}
// ValidateCreditVoucher sets the selected voucher as the active transaction voucher
func (h *MenuHandlers) ValidateCreditVoucher(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")
}
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
inputStr := string(input)
if inputStr == "0" || inputStr == "99" || inputStr == "88" || inputStr == "98" {
return res, nil
}
userStore := h.userdataStore
metadata, err := store.GetOrderedVoucherData(ctx, userStore, sessionId, inputStr)
if err != nil {
return res, fmt.Errorf("failed to retrieve swap to voucher data: %v", err)
}
if metadata == nil {
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher)
return res, nil
}
// Store the transaction voucher data
if err := store.StoreTransactionVoucher(ctx, h.userdataStore, sessionId, metadata); err != nil {
logg.ErrorCtxf(ctx, "failed on StoreTransactionVoucher", "error", err)
return res, err
}
// Store the state of the custom transaction voucher
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TRANSACTION_CUSTOM_VOUCHER_STATE, []byte("1"))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write custom transaction voucher", "key", storedb.DATA_TRANSACTION_CUSTOM_VOUCHER_STATE, "error", err)
return res, err
}
return res, nil
}

View File

@ -81,6 +81,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService)
ls.DbRs.AddLocalFunc("check_account_status", appHandlers.CheckAccountStatus)
ls.DbRs.AddLocalFunc("authorize_account", appHandlers.Authorize)
ls.DbRs.AddLocalFunc("quit", appHandlers.Quit)
ls.DbRs.AddLocalFunc("calc_credit_debt", appHandlers.CalculateCreditAndDebt)
ls.DbRs.AddLocalFunc("check_balance", appHandlers.CheckBalance)
ls.DbRs.AddLocalFunc("validate_recipient", appHandlers.ValidateRecipient)
ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset)
@ -104,12 +105,14 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService)
ls.DbRs.AddLocalFunc("get_profile_info", appHandlers.GetProfileInfo)
ls.DbRs.AddLocalFunc("verify_yob", appHandlers.VerifyYob)
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", appHandlers.ResetIncorrectYob)
ls.DbRs.AddLocalFunc("initiate_transaction", appHandlers.InitiateTransaction)
ls.DbRs.AddLocalFunc("normal_transaction_preview", appHandlers.NormalTransactionPreview)
ls.DbRs.AddLocalFunc("initiate_normal_transaction", appHandlers.InitiateNormalTransaction)
ls.DbRs.AddLocalFunc("confirm_pin_change", appHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", appHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_community_balance", appHandlers.FetchCommunityBalance)
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("set_voucher", appHandlers.SetVoucher)
ls.DbRs.AddLocalFunc("get_voucher_details", appHandlers.GetVoucherDetails)
@ -146,6 +149,14 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService)
ls.DbRs.AddLocalFunc("send_mpesa_min_limit", appHandlers.SendMpesaMinLimit)
ls.DbRs.AddLocalFunc("send_mpesa_preview", appHandlers.SendMpesaPreview)
ls.DbRs.AddLocalFunc("initiate_send_mpesa", appHandlers.InitiateSendMpesa)
ls.DbRs.AddLocalFunc("calculate_max_pay_debt", appHandlers.CalculateMaxPayDebt)
ls.DbRs.AddLocalFunc("confirm_debt_removal", appHandlers.ConfirmDebtRemoval)
ls.DbRs.AddLocalFunc("initiate_pay_debt", appHandlers.InitiatePayDebt)
ls.DbRs.AddLocalFunc("get_ordered_vouchers", appHandlers.GetOrderedVouchers)
ls.DbRs.AddLocalFunc("pool_deposit_max_amount", appHandlers.PoolDepositMaxAmount)
ls.DbRs.AddLocalFunc("confirm_pool_deposit", appHandlers.ConfirmPoolDeposit)
ls.DbRs.AddLocalFunc("initiate_pool_deposit", appHandlers.InitiatePoolDeposit)
ls.DbRs.AddLocalFunc("validate_credit_voucher", appHandlers.ValidateCreditVoucher)
ls.first = appHandlers.Init

View File

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

View File

@ -0,0 +1,10 @@
LOAD reset_transaction_amount 10
RELOAD reset_transaction_amount
MAP calculate_max_pay_debt
MOUT back 0
HALT
LOAD confirm_debt_removal 140
RELOAD confirm_debt_removal
CATCH invalid_pay_debt_amount flag_invalid_amount 1
INCMP _ 0
INCMP confirm_debt_removal *

View File

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

View File

@ -0,0 +1,10 @@
MAP confirm_debt_removal
MOUT back 0
MOUT quit 9
HALT
LOAD authorize_account 6
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
INCMP _ 0
INCMP quit 9
INCMP initiate_pay_debt *

View File

@ -13,7 +13,4 @@ LOAD validate_amount 64
RELOAD validate_amount
CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0
LOAD get_recipient 0
LOAD get_sender 64
LOAD get_amount 32
INCMP transaction_pin *

View File

@ -3,10 +3,13 @@ RELOAD transaction_reset
CATCH no_voucher flag_no_active_voucher 1
MOUT back 0
HALT
LOAD clear_trans_type_flag 6
RELOAD clear_trans_type_flag
LOAD validate_recipient 50
RELOAD validate_recipient
CATCH api_failure flag_api_call_error 1
CATCH api_failure flag_api_call_error 1
CATCH invalid_recipient flag_invalid_recipient 1
CATCH invite_recipient flag_invalid_recipient_with_invite 1
CATCH credit_vouchers flag_multiple_voucher 1
INCMP _ 0
INCMP credit_amount *

View File

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

View File

@ -0,0 +1,15 @@
LOAD get_ordered_vouchers 0
MAP get_ordered_vouchers
MOUT back 0
MOUT quit 99
MNEXT next 88
MPREV prev 98
HALT
INCMP > 88
INCMP < 98
INCMP _ 0
INCMP quit 99
LOAD validate_credit_voucher 67
RELOAD validate_credit_voucher
CATCH . flag_incorrect_voucher 1
INCMP credit_amount *

View File

@ -0,0 +1 @@
Pool deposit

View File

@ -0,0 +1 @@
Weka kwa bwawa

View File

@ -1 +1 @@
{{.get_mpesa_max_limit}}
{{.get_ordered_vouchers}}

View File

@ -1,10 +1,19 @@
CATCH no_voucher flag_no_active_voucher 1
LOAD get_mpesa_max_limit 0
RELOAD get_mpesa_max_limit
MAP get_mpesa_max_limit
LOAD get_ordered_vouchers 0
MAP get_ordered_vouchers
MOUT back 0
MOUT quit 9
MOUT quit 99
MNEXT next 88
MPREV prev 98
HALT
INCMP > 88
INCMP < 98
INCMP _ 0
INCMP quit 9
INCMP get_mpesa_confirmation *
INCMP quit 99
LOAD get_mpesa_max_limit 89
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 *

View File

@ -1,7 +1,4 @@
LOAD get_mpesa_preview 0
MAP get_mpesa_preview
CATCH api_failure flag_api_call_error 1
CATCH invalid_credit_send_amount flag_invalid_amount 1
MOUT back 0
MOUT quit 9
HALT

View File

@ -1 +1 @@
Get M-Pesa
Withdraw

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ msgid "Symbol: %s\nBalance: %s"
msgstr "Sarafu: %s\nSalio: %s"
msgid "Your request has been sent. You will receive an SMS when your %s %s has been swapped for %s."
msgstr "Ombi lako limetumwa. Utapokea SMS wakati %s %s yako itakapobadilishwa kuwa %s."
msgstr "Ombi lako limetumwa. Utapokea ujumbe wakati %s %s yako itakapobadilishwa kuwa %s."
msgid "%s balance: %s\n"
msgstr "%s salio: %s\n"
@ -59,16 +59,55 @@ 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"
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. You will receive ~ %s ksh"
msgstr "Ombi lako limetumwa. Utapokea ~ %s ksh"
msgid "Your request has been sent. Please await confirmation"
msgstr "Ombi lako limetumwa. Tafadhali subiri"
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"
msgid "You will get a prompt for your M-Pesa PIN shortly to send %s ksh and receive ~ %s cUSD"
msgstr "Utapokea kidokezo cha PIN yako ya M-Pesa hivi karibuni kutuma %s ksh na kupokea ~ %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 Mpesa hivi karibuni kutuma %s ksh na kupokea ~ %s %s"
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 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:(Kikomo: %s)"
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:"
msgid "Your active voucher %s is already set"
msgstr "Sarafu yako %s ishachaguliwa"
msgid "Select number or symbol from your vouchers:\n%s"
msgstr "Chagua nambari au ishara kutoka kwa sarafu zako:\n%s"
msgid "You will deposit %s %s into %s\n"
msgstr "Utaweka %s %s kwenye %s\n"
msgid "Your request has been sent. You will receive an SMS when %s %s has been deposited into %s."
msgstr "Ombi lako limetumwa. Utapokea ujumbe wakati %s %s itawekwa kwenye %s."
msgid "%s will receive %s %s from %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

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

View File

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

View File

@ -0,0 +1 @@
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

@ -3,7 +3,7 @@ RELOAD clear_temporary_value
LOAD manage_vouchers 160
RELOAD manage_vouchers
CATCH api_failure flag_api_call_error 1
LOAD check_balance 128
LOAD check_balance 148
RELOAD check_balance
MAP check_balance
MOUT send 1

View File

@ -1 +1 @@
{{.check_balance}}
{{.calc_credit_debt}}

View File

@ -1,9 +1,18 @@
MAP check_balance
MOUT get_mpesa 1
MOUT send_mpesa 2
LOAD calc_credit_debt 150
RELOAD calc_credit_debt
CATCH api_failure flag_api_call_error 1
MAP calc_credit_debt
MOUT pay_debt 1
MOUT deposit 2
MOUT get_mpesa 3
MOUT send_mpesa 4
MOUT back 0
MOUT quit 9
HALT
INCMP get_mpesa 1
INCMP send_mpesa 2
INCMP ^ 0
INCMP pay_debt 1
INCMP pool_deposit 2
INCMP get_mpesa 3
INCMP send_mpesa 4
INCMP quit 9
INCMP . *

View File

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

View File

@ -0,0 +1,13 @@
LOAD reset_transaction_amount 10
RELOAD reset_transaction_amount
MAP get_mpesa_max_limit
MOUT back 0
MOUT quit 9
HALT
INCMP _ 0
INCMP quit 9
LOAD get_mpesa_preview 90
RELOAD get_mpesa_preview
CATCH api_failure flag_api_call_error 1
CATCH invalid_get_mpesa_amount flag_invalid_amount 1
INCMP get_mpesa_confirmation *

View File

@ -0,0 +1 @@
No stable voucher found

View File

@ -0,0 +1,5 @@
MOUT back 0
MOUT quit 9
HALT
INCMP ^ 0
INCMP quit 9

View File

@ -0,0 +1 @@
Hakuna sarafu thabiti iliyopatikana

View File

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

View File

@ -0,0 +1,18 @@
CATCH no_voucher flag_no_active_voucher 1
LOAD get_paydebt_voucher_list 0
MAP get_paydebt_voucher_list
MOUT back 0
MOUT quit 99
MNEXT next 88
MPREV prev 98
HALT
INCMP > 88
INCMP < 98
INCMP _ 0
INCMP quit 99
LOAD calculate_max_pay_debt 0
RELOAD calculate_max_pay_debt
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 *

View File

@ -0,0 +1 @@
Pay debt

View File

@ -0,0 +1 @@
Lipa deni

View File

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

View File

@ -0,0 +1,16 @@
CATCH no_voucher flag_no_active_voucher 1
LOAD get_ordered_vouchers 0
MAP get_ordered_vouchers
MOUT back 0
MOUT quit 99
MNEXT next 88
MPREV prev 98
HALT
INCMP > 88
INCMP < 98
INCMP _ 0
INCMP quit 99
LOAD pool_deposit_max_amount 120
RELOAD pool_deposit_max_amount
CATCH . flag_incorrect_voucher 1
INCMP pool_deposit_amount *

View File

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

View File

@ -0,0 +1,10 @@
LOAD reset_transaction_amount 10
RELOAD reset_transaction_amount
MAP pool_deposit_max_amount
MOUT back 0
HALT
LOAD confirm_pool_deposit 140
RELOAD confirm_pool_deposit
CATCH invalid_pool_deposit_amount flag_invalid_amount 1
INCMP _ 0
INCMP pool_deposit_confirmation *

View File

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

View File

@ -0,0 +1,10 @@
MAP confirm_pool_deposit
MOUT back 0
MOUT quit 9
HALT
LOAD authorize_account 6
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
INCMP _ 0
INCMP quit 9
INCMP pool_deposit_initiated *

View File

@ -0,0 +1,2 @@
{{.confirm_pool_deposit}}
Tafadhali weka PIN yako kudhibitisha:

View File

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

View File

@ -36,3 +36,5 @@ 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
flag,flag_no_stable_vouchers,46,this is set when the user does not have a stable voucher
flag,flag_multiple_voucher,47,this is set when the user only has a multiple voucher

1 flag flag_language_set 8 checks whether the user has set their prefered language
36 flag flag_low_swap_amount 43 this is set when the swap max limit is less than 0.1
37 flag flag_alias_unavailable 44 this is set when the preferred alias is not available
38 flag flag_swap_transaction 45 this is set when the transaction will involve performing a swap
39 flag flag_no_stable_vouchers 46 this is set when the user does not have a stable voucher
40 flag flag_multiple_voucher 47 this is set when the user only has a multiple voucher

View File

@ -7,7 +7,7 @@ CATCH blocked_account flag_account_blocked 1
CATCH select_language flag_language_set 0
CATCH terms flag_account_created 0
CATCH create_pin flag_pin_set 0
LOAD check_account_status 0
LOAD check_account_status 40
RELOAD check_account_status
CATCH api_failure flag_api_call_error 1
CATCH account_pending flag_account_pending 1

View File

@ -1,2 +1 @@
Select number or symbol from your vouchers:
{{.get_vouchers}}
{{.get_voucher_list}}

View File

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

View File

@ -1,2 +0,0 @@
Chagua nambari au ishara kutoka kwa salio zako:
{{.get_vouchers}}

View File

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

View File

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

View File

@ -1 +1 @@
Send M-Pesa
Top-up

View File

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

View File

@ -1,10 +1,4 @@
LOAD reset_incorrect_pin 6
CATCH _ flag_account_authorized 0
RELOAD get_amount
MAP get_amount
RELOAD get_recipient
MAP get_recipient
RELOAD get_sender
MAP get_sender
LOAD initiate_transaction 0
LOAD initiate_normal_transaction 0
HALT

View File

@ -1,2 +1,2 @@
{{.get_recipient}} will receive {{.get_amount}} from {{.get_sender}}
{{.normal_transaction_preview}}
Please enter your PIN to confirm:

View File

@ -1,9 +1,5 @@
RELOAD get_amount
MAP get_amount
RELOAD get_recipient
MAP get_recipient
RELOAD get_sender
MAP get_sender
LOAD normal_transaction_preview 0
MAP normal_transaction_preview
MOUT back 0
MOUT quit 9
LOAD authorize_account 6

View File

@ -1,2 +0,0 @@
{{.get_recipient}} atapokea {{.get_amount}} kutoka kwa {{.get_sender}}
Tafadhali weka PIN yako kudhibitisha:

View File

@ -65,7 +65,7 @@ const (
DATA_ACCOUNT_ALIAS
//currently suggested alias by the api awaiting user's confirmation as accepted account alias
DATA_SUGGESTED_ALIAS
//Key used to store a value of 1 for a user to reset their own PIN once they access the menu.
//Key used to store a value of 1 for a user to reset their own PIN once they access the menu.
DATA_SELF_PIN_RESET
// Holds the active pool contract address for the swap
DATA_ACTIVE_POOL_ADDRESS
@ -93,6 +93,14 @@ const (
DATA_SEND_TRANSACTION_TYPE
// Holds the recipient formatted phone number
DATA_RECIPIENT_PHONE_NUMBER
// Currently active swap from balance for the swap
DATA_ACTIVE_SWAP_FROM_BALANCE
// Holds the state whether the transaction uses a custom voucher
DATA_TRANSACTION_CUSTOM_VOUCHER_STATE
// Holds the initial recipient input given by the user
DATA_RECIPIENT_INPUT
// Holds the transaction voucher
DATA_TRANSACTION_CUSTOM_VOUCHER
)
const (
@ -104,7 +112,14 @@ const (
DATA_VOUCHER_DECIMALS
// List of voucher EVM addresses for vouchers valid in the user context.
DATA_VOUCHER_ADDRESSES
// List of senders for valid transactions in the user context.
// List of ordered voucher symbols in the user context.
DATA_ORDERED_VOUCHER_SYMBOLS
// List of ordered voucher balances in the user context.
DATA_ORDERED_VOUCHER_BALANCES
// List of ordered voucher decimals in the user context.
DATA_ORDERED_VOUCHER_DECIMALS
// List of ordered voucher EVM addresses in the user context.
DATA_ORDERED_VOUCHER_ADDRESSES
)
const (

View File

@ -18,6 +18,7 @@ type SwapData struct {
ActiveSwapFromAddress string
ActiveSwapToSym string
ActiveSwapToAddress string
ActiveSwapToDecimal string
}
type SwapPreviewData struct {
@ -43,6 +44,7 @@ func ReadSwapData(ctx context.Context, store DataStore, sessionId string) (SwapD
"ActiveSwapFromAddress": storedb.DATA_ACTIVE_ADDRESS,
"ActiveSwapToSym": storedb.DATA_ACTIVE_SWAP_TO_SYM,
"ActiveSwapToAddress": storedb.DATA_ACTIVE_SWAP_TO_ADDRESS,
"ActiveSwapToDecimal": storedb.DATA_ACTIVE_SWAP_TO_DECIMAL,
}
v := reflect.ValueOf(&data).Elem()
@ -187,3 +189,74 @@ func UpdateSwapToVoucherData(ctx context.Context, store DataStore, sessionId str
return nil
}
// UpdateSwapFromVoucherData updates the active swap from voucher data in the DataStore.
func UpdateSwapFromVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
logg.InfoCtxf(ctx, "UpdateSwapFromVoucherData", "data", data)
// Active swap to voucher data entries
activeEntries := map[storedb.DataTyp][]byte{
storedb.DATA_ACTIVE_SWAP_FROM_SYM: []byte(data.TokenSymbol),
storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: []byte(data.TokenDecimals),
storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS: []byte(data.TokenAddress),
storedb.DATA_ACTIVE_SWAP_FROM_BALANCE: []byte(data.Balance),
}
// Write active data
for key, value := range activeEntries {
if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
return err
}
}
return nil
}
// ReadSwapFromVoucher retrieves the voucher being swapped into the pool (swap from)
func ReadSwapFromVoucher(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
keys := []storedb.DataTyp{
storedb.DATA_ACTIVE_SWAP_FROM_SYM,
storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL,
storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS,
storedb.DATA_ACTIVE_SWAP_FROM_BALANCE,
}
data := make(map[storedb.DataTyp]string)
for _, key := range keys {
value, err := store.ReadEntry(ctx, sessionId, key)
if err != nil {
return nil, fmt.Errorf("failed to get data key %x: %v", key, err)
}
data[key] = string(value)
}
return &dataserviceapi.TokenHoldings{
TokenSymbol: data[storedb.DATA_ACTIVE_SWAP_FROM_SYM],
Balance: data[storedb.DATA_ACTIVE_SWAP_FROM_BALANCE],
TokenDecimals: data[storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL],
TokenAddress: data[storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS],
}, nil
}
// ReadSwapToVoucher retrieves the swap to voucher being swapped from the pool
func ReadSwapToVoucher(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
keys := []storedb.DataTyp{
storedb.DATA_ACTIVE_SWAP_TO_SYM,
storedb.DATA_ACTIVE_SWAP_TO_DECIMAL,
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS,
}
data := make(map[storedb.DataTyp]string)
for _, key := range keys {
value, err := store.ReadEntry(ctx, sessionId, key)
if err != nil {
return nil, fmt.Errorf("failed to get data key %x: %v", key, err)
}
data[key] = string(value)
}
return &dataserviceapi.TokenHoldings{
TokenSymbol: data[storedb.DATA_ACTIVE_SWAP_TO_SYM],
TokenDecimals: data[storedb.DATA_ACTIVE_SWAP_TO_DECIMAL],
TokenAddress: data[storedb.DATA_ACTIVE_SWAP_TO_ADDRESS],
}, nil
}

View File

@ -13,7 +13,7 @@ import (
)
type TransactionData struct {
TemporaryValue string
RecipientInput string
ActiveSym string
Amount string
PublicKey string
@ -77,7 +77,7 @@ func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) {
func ReadTransactionData(ctx context.Context, store DataStore, sessionId string) (TransactionData, error) {
data := TransactionData{}
fieldToKey := map[string]storedb.DataTyp{
"TemporaryValue": storedb.DATA_TEMPORARY_VALUE,
"RecipientInput": storedb.DATA_RECIPIENT_INPUT,
"ActiveSym": storedb.DATA_ACTIVE_SYM,
"Amount": storedb.DATA_AMOUNT,
"PublicKey": storedb.DATA_PUBLIC_KEY,

View File

@ -221,7 +221,7 @@ func TestReadTransactionData(t *testing.T) {
// Test transaction data
transactionData := map[storedb.DataTyp]string{
storedb.DATA_TEMPORARY_VALUE: "0712345678",
storedb.DATA_RECIPIENT_INPUT: "0712345678",
storedb.DATA_ACTIVE_SYM: "SRF",
storedb.DATA_AMOUNT: "1000000",
storedb.DATA_PUBLIC_KEY: publicKey,
@ -238,7 +238,7 @@ func TestReadTransactionData(t *testing.T) {
}
expectedResult := TransactionData{
TemporaryValue: "0712345678",
RecipientInput: "0712345678",
ActiveSym: "SRF",
Amount: "1000000",
PublicKey: publicKey,

View File

@ -120,8 +120,49 @@ func GetVoucherData(ctx context.Context, store DataStore, sessionId string, inpu
}, nil
}
// GetOrderedVoucherData retrieves and matches ordered voucher data
func GetOrderedVoucherData(ctx context.Context, store DataStore, sessionId string, input string) (*dataserviceapi.TokenHoldings, error) {
keys := []storedb.DataTyp{
storedb.DATA_ORDERED_VOUCHER_SYMBOLS,
storedb.DATA_ORDERED_VOUCHER_BALANCES,
storedb.DATA_ORDERED_VOUCHER_DECIMALS,
storedb.DATA_ORDERED_VOUCHER_ADDRESSES,
}
data := make(map[storedb.DataTyp]string)
for _, key := range keys {
value, err := store.ReadEntry(ctx, sessionId, key)
if err != nil {
return nil, fmt.Errorf("failed to get data key %x: %v", key, err)
}
data[key] = string(value)
}
symbol, balance, decimal, address := MatchVoucher(input,
data[storedb.DATA_ORDERED_VOUCHER_SYMBOLS],
data[storedb.DATA_ORDERED_VOUCHER_BALANCES],
data[storedb.DATA_ORDERED_VOUCHER_DECIMALS],
data[storedb.DATA_ORDERED_VOUCHER_ADDRESSES],
)
if symbol == "" {
return nil, nil
}
return &dataserviceapi.TokenHoldings{
TokenSymbol: string(symbol),
Balance: string(balance),
TokenDecimals: string(decimal),
TokenAddress: string(address),
}, nil
}
// 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) {
// case for invalid input with no current symbols
if symbols == "" {
return
}
symList := strings.Split(symbols, "\n")
balList := strings.Split(balances, "\n")
decList := strings.Split(decimals, "\n")
@ -148,20 +189,20 @@ func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol,
return
}
// StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore.
func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
// StoreTransactionVoucher saves voucher metadata in the DataStore.
func StoreTransactionVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
tempData := fmt.Sprintf("%s,%s,%s,%s", data.TokenSymbol, data.Balance, data.TokenDecimals, data.TokenAddress)
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(tempData)); err != nil {
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_TRANSACTION_CUSTOM_VOUCHER, []byte(tempData)); err != nil {
return err
}
return nil
}
// GetTemporaryVoucherData retrieves temporary voucher metadata from the DataStore.
func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
temp_data, err := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
// GetTransactionVoucherData retrieves the transaction voucher metadata from the DataStore.
func GetTransactionVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
temp_data, err := store.ReadEntry(ctx, sessionId, storedb.DATA_TRANSACTION_CUSTOM_VOUCHER)
if err != nil {
return nil, err
}
@ -225,3 +266,46 @@ func FormatVoucherList(ctx context.Context, symbolsData, balancesData string) []
}
return combined
}
// AddDecimalStrings adds two decimal numbers represented as strings
// and returns the result as a string without losing precision.
func AddDecimalStrings(a, b string) string {
x, ok := new(big.Rat).SetString(a)
if !ok {
x = new(big.Rat)
}
y, ok := new(big.Rat).SetString(b)
if !ok {
y = new(big.Rat)
}
x.Add(x, y)
// Convert back to string without scientific notation
return x.FloatString(maxDecimalPlaces(x, y))
}
// maxDecimalPlaces ensures we preserve enough decimal precision
func maxDecimalPlaces(rats ...*big.Rat) int {
max := 0
for _, r := range rats {
if r == nil {
continue
}
if d := decimalPlaces(r); d > max {
max = d
}
}
return max
}
func decimalPlaces(r *big.Rat) int {
s := r.FloatString(18)
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '.' {
return len(s) - i - 1
}
}
return 0
}

View File

@ -118,7 +118,7 @@ func TestStoreTemporaryVoucher(t *testing.T) {
}
// Execute the function being tested
err := StoreTemporaryVoucher(ctx, store, sessionId, voucherData)
err := StoreTransactionVoucher(ctx, store, sessionId, voucherData)
require.NoError(t, err)
// Verify stored data
@ -142,11 +142,11 @@ func TestGetTemporaryVoucherData(t *testing.T) {
}
// Store the data
err := StoreTemporaryVoucher(ctx, store, sessionId, tempData)
err := StoreTransactionVoucher(ctx, store, sessionId, tempData)
require.NoError(t, err)
// Execute the function being tested
data, err := GetTemporaryVoucherData(ctx, store, sessionId)
data, err := GetTransactionVoucherData(ctx, store, sessionId)
require.NoError(t, err)
require.Equal(t, tempData, data)
}
@ -170,7 +170,7 @@ func TestUpdateVoucherData(t *testing.T) {
TokenDecimals: "8",
TokenAddress: "0xold",
}
require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData))
require.NoError(t, StoreTransactionVoucher(ctx, store, sessionId, tempData))
// Execute update
err := UpdateVoucherData(ctx, store, sessionId, newData)