Compare commits

..

54 Commits

Author SHA1 Message Date
fc2ca0f546 Merge pull request 'send with swap' (#102) from send-with-swap into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #102
2025-11-12 08:54:05 +01:00
724bc1bcf3 Merge branch 'master' into send-with-swap
Some checks failed
release / docker (push) Has been cancelled
2025-11-05 15:32:35 +03:00
6e8c0fbcb3 Merge pull request 'Fix get_pools being skipped' (#108) from pool-debug into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #108
2025-11-05 12:12:14 +01:00
9f8cf95e0f
remove print debug statements 2025-11-05 11:20:55 +03:00
97be6e943c
add a reload for the get_pools 2025-11-05 11:16:52 +03:00
ba93bd9152
add debug logs for the pool data entry 2025-11-05 11:03:45 +03:00
5fac27d00e
add debug logs for the pool keys 2025-11-04 15:27:15 +03:00
348185ef96
use l.Get for the language change
Some checks failed
release / docker (push) Has been cancelled
2025-10-30 12:36:11 +03:00
f51f577e2a
added the swahili translations 2025-10-30 12:35:46 +03:00
582f349be3
handle normal send transactions based on the sym 2025-10-30 11:12:16 +03:00
8ce17a8d1e
add a sym for the credit_max_amount 2025-10-30 11:11:39 +03:00
4092437d21
add a node for the credit-send amount 2025-10-30 11:11:12 +03:00
37f4b60679
removed the credit-swap related code 2025-10-30 11:08:57 +03:00
878b5d0aa5
added the credit_send top-level menu 2025-10-30 11:08:18 +03:00
d2b934feda
use the same translation from the english menu 2025-10-30 10:07:05 +03:00
4c80606b56
properly handle formatting, preventing rounding errors for the case of 2.1 -> 2.09
Some checks failed
release / docker (push) Has been cancelled
2025-10-29 10:55:42 +03:00
38ab1ecdd1
added an edge-case test for precision 2025-10-29 10:53:11 +03:00
a49257657e
switch the order to match the TokenHoldings struct
Some checks failed
release / docker (push) Has been cancelled
2025-10-28 17:20:31 +03:00
95b48371ce
CATCH invalid credit-swap amounts 2025-10-28 17:19:40 +03:00
9da2f8a6ac
have a node for invalid credit send amounts 2025-10-28 17:17:22 +03:00
3194508e51
increase the output sizes 2025-10-28 17:16:54 +03:00
41f08c5c9b
display the max_amount directly from the function 2025-10-28 17:16:25 +03:00
de539dc300
update the credit-send functionality to display the RAT data 2025-10-28 17:15:39 +03:00
8af2ccd36f
added text translations for the amounts (for normal and credit-send transactions) 2025-10-28 17:09:46 +03:00
d9c49ee119
upgraded sarafu-api to make use of the credit-send API 2025-10-28 12:21:42 +03:00
5cfdd949ff
use the correct amount when initiating the transfer (the amount that has been swapped)
Some checks failed
release / docker (push) Has been cancelled
2025-10-22 17:32:35 +03:00
865dae4b7f
correctly format amounts to 2 decimal places using TruncateDecimalString 2025-10-22 16:16:05 +03:00
0af41ea1f1
Merge branch 'master' into send-with-swap 2025-10-22 14:40:21 +03:00
ba9a23946f
added ClearTransactionTypeFlag when a user goes back
Some checks failed
release / docker (push) Has been cancelled
2025-10-09 15:08:51 +03:00
34271cba23 Merge branch 'master' into send-with-swap
Some checks failed
release / docker (push) Has been cancelled
2025-10-09 14:32:31 +03:00
e029a8cdc7
improved the tests by updating the menu 2025-10-09 14:19:03 +03:00
9878745861
fixed the TestCheckBalance by adjusting the expected result 2025-10-09 13:39:30 +03:00
606a551f60
use the correct data keys on the test 2025-10-09 13:31:55 +03:00
ed780659d3
removed the debug statements 2025-10-09 13:20:58 +03:00
764431ee58
added some debug statements 2025-10-09 13:08:22 +03:00
fb5eb3f24f
added the determineAndSaveTransactionType heper for shared logic on the transaction type 2025-10-09 12:45:16 +03:00
27fdb20974
handle the transaction type for a swap when using an alias 2025-10-09 12:27:51 +03:00
f7859fb72a
handle the transaction type for a swap when using an address 2025-10-09 12:15:56 +03:00
7973ecdfd9 Merge branch 'master' into send-with-swap
Some checks failed
release / docker (push) Has been cancelled
2025-08-26 12:22:50 +03:00
c90d3cd731
added the handler functions 2025-08-25 17:28:11 +03:00
cda2d49f3e
added TransactionInitiateSwap functionality that performs a swap followed by a send 2025-08-25 17:27:20 +03:00
4492f8087a
added TransactionSwapPreview functionality 2025-08-25 17:25:59 +03:00
0f8c2f9270
use the recipient's phone number to read swap related data 2025-08-25 17:24:58 +03:00
5a09d33be0
removed unused DATA_RECIPIENT_ACTIVE_TOKEN key 2025-08-25 17:21:23 +03:00
0c67efedea
added the transaction_swap_initiated node 2025-08-25 17:20:34 +03:00
14d493475e
catch the flag_swap_transaction and move to the transaction_swap node 2025-08-25 17:20:07 +03:00
0e4dfe1baf
added the flag_swap_transaction when a swap needs to be performed in the send node 2025-08-25 17:19:11 +03:00
7e1042c6a9
update the MaxAmount logic to check the swap capability for swap transactions 2025-08-25 12:46:16 +03:00
e274967c8e
update the handlePhoneNumber logic to cover new users or those without an active voucher 2025-08-25 10:52:22 +03:00
Alfred Kamanda
5b82afa768
added the DATA_RECIPIENT_PHONE_NUMBER to store the formatted phone number 2025-08-04 13:50:21 +03:00
Alfred Kamanda
b6de057cc4
add a reset for the DATA_RECIPIENT_ACTIVE_TOKEN key 2025-08-04 11:21:14 +03:00
Alfred Kamanda
758463ee8c
add transaction data keys 2025-08-04 11:19:22 +03:00
Alfred Kamanda
f441b3b2af
split the ValidateRecipient and check the transaction type 2025-08-04 11:18:45 +03:00
Alfred Kamanda
4d687cac2e
updated the MaxAmount description comment 2025-07-30 11:46:22 +03:00
43 changed files with 854 additions and 240 deletions

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.23.4
require ( require (
git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66 git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66
git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20250417111317-2953f4c2f32e git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20250417111317-2953f4c2f32e
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63 git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028083421-fe897cca84f2
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306 git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306
git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694
github.com/alecthomas/assert/v2 v2.2.2 github.com/alecthomas/assert/v2 v2.2.2

4
go.sum
View File

@ -24,6 +24,10 @@ git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251021120522-
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251021120522-6f7802b58cf5/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8= git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251021120522-6f7802b58cf5/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63 h1:yznaUXeAy+qiZb2nCxosYXE5HyCPpynIoplEuYV/zQM= git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63 h1:yznaUXeAy+qiZb2nCxosYXE5HyCPpynIoplEuYV/zQM=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8= git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028081048-a705443786fd h1:VIj5OdRae2wfE6NdLp6ZdHv0jtRbOeRURYQCU29RWBM=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028081048-a705443786fd/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028083421-fe897cca84f2 h1:wf//obTSLW5VZ0gM25l0U5oV/d+TBXX+1ClSMkEU7Uc=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251028083421-fe897cca84f2/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306 h1:Jo+yWysWw/N5BJQtAyEMN8ePVvAyPHv+JG4lQti+1N4= git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306 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 v0.9.0-beta.2.0.20250408094335-e2d1f65bb306/go.mod h1:FdLwYtzsjOIcDiW4uDgDYnB4Wdzq12uJUe0QHSSPbSo=
git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 h1:DjJlBSz0S13acft5XZDWk7ZYnzElym0xLMYEVgyNJ+E= git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 h1:DjJlBSz0S13acft5XZDWk7ZYnzElym0xLMYEVgyNJ+E=

View File

@ -52,7 +52,7 @@ func TestCheckBalance(t *testing.T) {
alias: "user72", alias: "user72",
activeSym: "SRF", activeSym: "SRF",
activeBal: "10.967", activeBal: "10.967",
expectedResult: resource.Result{Content: "user72 balance: 10.96 SRF\n"}, expectedResult: resource.Result{Content: "user72\nBalance: 10.96 SRF\n"},
expectError: false, expectError: false,
}, },
} }

View File

@ -220,7 +220,7 @@ func (h *MenuHandlers) SwapMaxLimit(ctx context.Context, sym string, input []byt
} }
// Format to 2 decimal places // Format to 2 decimal places
maxStr := fmt.Sprintf("%.2f", maxAmountFloat) maxStr, _ := store.TruncateDecimalString(string(maxAmountStr), 2)
if maxAmountFloat < 0.1 { if maxAmountFloat < 0.1 {
// return with low amount flag // return with low amount flag
@ -319,14 +319,9 @@ func (h *MenuHandlers) SwapPreview(ctx context.Context, sym string, input []byte
// Scale down the quoted amount // Scale down the quoted amount
quoteAmountStr := store.ScaleDownBalance(r.OutValue, swapData.ActiveSwapToDecimal) quoteAmountStr := store.ScaleDownBalance(r.OutValue, swapData.ActiveSwapToDecimal)
qouteAmount, err := strconv.ParseFloat(quoteAmountStr, 64)
if err != nil {
logg.ErrorCtxf(ctx, "failed to parse quoteAmountStr as float", "value", quoteAmountStr, "error", err)
return res, err
}
// Format to 2 decimal places // Format to 2 decimal places
qouteStr := fmt.Sprintf("%.2f", qouteAmount) qouteStr, _ := store.TruncateDecimalString(string(quoteAmountStr), 2)
res.Content = fmt.Sprintf( res.Content = fmt.Sprintf(
"You will swap:\n%s %s for %s %s:", "You will swap:\n%s %s for %s %s:",

View File

@ -9,135 +9,239 @@ import (
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/common/hex"
"git.grassecon.net/grassrootseconomics/common/identity" "git.grassecon.net/grassrootseconomics/common/identity"
"git.grassecon.net/grassrootseconomics/common/phone" "git.grassecon.net/grassrootseconomics/common/phone"
"git.grassecon.net/grassrootseconomics/sarafu-api/models"
"git.grassecon.net/grassrootseconomics/sarafu-api/remote/http" "git.grassecon.net/grassrootseconomics/sarafu-api/remote/http"
"git.grassecon.net/grassrootseconomics/sarafu-vise/config" "git.grassecon.net/grassrootseconomics/sarafu-vise/config"
"git.grassecon.net/grassrootseconomics/sarafu-vise/store" "git.grassecon.net/grassrootseconomics/sarafu-vise/store"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
"github.com/grassrootseconomics/ethutils" "github.com/grassrootseconomics/ethutils"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
"gopkg.in/leonelquinteros/gotext.v1" "gopkg.in/leonelquinteros/gotext.v1"
) )
// ValidateRecipient validates that the given input is valid. // ValidateRecipient validates that the given input is valid.
//
// TODO: split up functino
func (h *MenuHandlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *MenuHandlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var AliasAddressResult string
var AliasAddress *models.AliasAddress
store := h.userdataStore store := h.userdataStore
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
// remove white spaces // remove white spaces
recipient := strings.ReplaceAll(string(input), " ", "") recipient := strings.ReplaceAll(string(input), " ", "")
if recipient == "0" {
return res, nil
}
if recipient != "0" { recipientType, err := identity.CheckRecipient(recipient)
recipientType, err := identity.CheckRecipient(recipient) if err != nil {
if err != nil { // Invalid recipient format (not a phone number, address, or valid alias format)
// Invalid recipient format (not a phone number, address, or valid alias format) res.FlagSet = append(res.FlagSet, flag_invalid_recipient)
res.FlagSet = append(res.FlagSet, flag_invalid_recipient) res.Content = recipient
return res, nil
}
// save the recipient as the temporaryRecipient
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(recipient))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryRecipient entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", recipient, "error", err)
return res, err
}
switch recipientType {
case "phone number":
return h.handlePhoneNumber(ctx, sessionId, recipient, &res)
case "address":
return h.handleAddress(ctx, sessionId, recipient, &res)
case "alias":
return h.handleAlias(ctx, sessionId, recipient, &res)
}
return res, nil
}
func (h *MenuHandlers) handlePhoneNumber(ctx context.Context, sessionId, recipient string, res *resource.Result) (resource.Result, error) {
store := h.userdataStore
flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite")
formattedNumber, err := phone.FormatPhoneNumber(recipient)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to format phone number", "recipient", recipient, "error", err)
return *res, err
}
publicKey, err := store.ReadEntry(ctx, formattedNumber, storedb.DATA_PUBLIC_KEY)
if err != nil {
if db.IsNotFound(err) {
logg.InfoCtxf(ctx, "Unregistered phone number", "recipient", recipient)
res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite)
res.Content = recipient res.Content = recipient
return *res, nil
return res, nil
} }
logg.ErrorCtxf(ctx, "Failed to read publicKey", "error", err)
return *res, err
}
// save the recipient as the temporaryRecipient if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, publicKey); err != nil {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(recipient)) logg.ErrorCtxf(ctx, "Failed to write recipient", "value", string(publicKey), "error", err)
if err != nil { return *res, err
logg.ErrorCtxf(ctx, "failed to write temporaryRecipient entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", recipient, "error", err) }
return res, err
// Delegate to shared logic
if err := h.determineAndSaveTransactionType(ctx, sessionId, publicKey, []byte(formattedNumber)); err != nil {
return *res, err
}
return *res, nil
}
func (h *MenuHandlers) handleAddress(ctx context.Context, sessionId, recipient string, res *resource.Result) (resource.Result, error) {
store := h.userdataStore
address := ethutils.ChecksumAddress(recipient)
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(address)); err != nil {
logg.ErrorCtxf(ctx, "Failed to write recipient address", "error", err)
return *res, err
}
// normalize the address to fetch the recipient's phone number
publicKeyNormalized, err := hex.NormalizeHex(address)
if err != nil {
return *res, err
}
// get the recipient's phone number from the address
recipientPhoneNumber, err := store.ReadEntry(ctx, publicKeyNormalized, storedb.DATA_PUBLIC_KEY_REVERSE)
if err != nil || len(recipientPhoneNumber) == 0 {
logg.WarnCtxf(ctx, "Recipient address not registered, switching to normal transaction", "address", address)
recipientPhoneNumber = nil
}
if err := h.determineAndSaveTransactionType(ctx, sessionId, []byte(address), recipientPhoneNumber); err != nil {
return *res, err
}
return *res, nil
}
func (h *MenuHandlers) handleAlias(ctx context.Context, sessionId, recipient string, res *resource.Result) (resource.Result, error) {
store := h.userdataStore
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
var aliasAddressResult string
if strings.Contains(recipient, ".") {
alias, err := h.accountService.CheckAliasAddress(ctx, recipient)
if err == nil {
aliasAddressResult = alias.Address
} else {
logg.ErrorCtxf(ctx, "Failed to resolve alias", "alias", recipient, "error", err)
} }
} else {
for _, domain := range config.SearchDomains() {
fqdn := fmt.Sprintf("%s.%s", recipient, domain)
logg.InfoCtxf(ctx, "Trying alias", "fqdn", fqdn)
switch recipientType { alias, err := h.accountService.CheckAliasAddress(ctx, fqdn)
case "phone number": if err == nil {
// format the phone number res.FlagReset = append(res.FlagReset, flag_api_error)
formattedNumber, err := phone.FormatPhoneNumber(recipient) aliasAddressResult = alias.Address
if err != nil { break
logg.ErrorCtxf(ctx, "Failed to format the phone number: %s", recipient, "error", err)
return res, err
}
// Check if the phone number is registered
publicKey, err := store.ReadEntry(ctx, formattedNumber, storedb.DATA_PUBLIC_KEY)
if err != nil {
if db.IsNotFound(err) {
logg.InfoCtxf(ctx, "Unregistered phone number: %s", recipient)
res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite)
res.Content = recipient
return res, nil
}
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", storedb.DATA_PUBLIC_KEY, "error", err)
return res, err
}
// Save the publicKey as the recipient
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, publicKey)
if err != nil {
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", string(publicKey), "error", err)
return res, err
}
case "address":
// checksum the address
address := ethutils.ChecksumAddress(recipient)
// Save the valid Ethereum address as the recipient
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(address))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", recipient, "error", err)
return res, err
}
case "alias":
if strings.Contains(recipient, ".") {
AliasAddress, err = h.accountService.CheckAliasAddress(ctx, recipient)
if err == nil {
AliasAddressResult = AliasAddress.Address
} else {
logg.ErrorCtxf(ctx, "failed to resolve alias", "alias", recipient, "error_alias_check", err)
}
} else { } else {
//Perform a search for each search domain,break on first match res.FlagSet = append(res.FlagSet, flag_api_error)
for _, domain := range config.SearchDomains() { logg.ErrorCtxf(ctx, "Alias resolution failed", "alias", fqdn, "error", err)
fqdn := fmt.Sprintf("%s.%s", recipient, domain) return *res, nil
logg.InfoCtxf(ctx, "Resolving with fqdn alias", "alias", fqdn)
AliasAddress, err = h.accountService.CheckAliasAddress(ctx, fqdn)
if err == nil {
res.FlagReset = append(res.FlagReset, flag_api_error)
AliasAddressResult = AliasAddress.Address
continue
} else {
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed to resolve alias", "alias", recipient, "error_alias_check", err)
return res, nil
}
}
}
if AliasAddressResult == "" {
res.Content = recipient
res.FlagSet = append(res.FlagSet, flag_invalid_recipient)
return res, nil
} else {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(AliasAddressResult))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", AliasAddressResult, "error", err)
return res, err
}
} }
} }
} }
return res, nil if aliasAddressResult == "" {
res.FlagSet = append(res.FlagSet, flag_invalid_recipient)
res.Content = recipient
return *res, nil
}
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(aliasAddressResult)); err != nil {
logg.ErrorCtxf(ctx, "Failed to store alias recipient", "error", err)
return *res, err
}
// Normalize the alias address to fetch the recipient's phone number
publicKeyNormalized, err := hex.NormalizeHex(aliasAddressResult)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to normalize alias address", "address", aliasAddressResult, "error", err)
return *res, err
}
// get the recipient's phone number from the address
recipientPhoneNumber, err := store.ReadEntry(ctx, publicKeyNormalized, storedb.DATA_PUBLIC_KEY_REVERSE)
if err != nil || len(recipientPhoneNumber) == 0 {
logg.WarnCtxf(ctx, "Alias address not registered, switching to normal transaction", "address", aliasAddressResult)
recipientPhoneNumber = nil
}
if err := h.determineAndSaveTransactionType(ctx, sessionId, []byte(aliasAddressResult), recipientPhoneNumber); err != nil {
return *res, err
}
return *res, nil
}
// determineAndSaveTransactionType centralizes transaction-type logic and recipient info persistence.
// It expects the session to already have the recipient's public key (address) written.
func (h *MenuHandlers) determineAndSaveTransactionType(
ctx context.Context,
sessionId string,
publicKey []byte,
recipientPhoneNumber []byte,
) error {
store := h.userdataStore
txType := "swap"
// Read sender's active address
senderActiveAddress, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to read sender active address", "error", err)
return err
}
var recipientActiveAddress []byte
if recipientPhoneNumber != nil {
recipientActiveAddress, _ = store.ReadEntry(ctx, string(recipientPhoneNumber), storedb.DATA_ACTIVE_ADDRESS)
}
// recipient has no active token → normal transaction
if recipientActiveAddress == nil {
txType = "normal"
} else if senderActiveAddress != nil && string(senderActiveAddress) == string(recipientActiveAddress) {
// recipient has active token same as sender → normal transaction
txType = "normal"
}
// Save the transaction type
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte(txType)); err != nil {
logg.ErrorCtxf(ctx, "Failed to write transaction type", "type", txType, "error", err)
return err
}
// Save the recipient's phone number only if it exists
if recipientPhoneNumber != nil {
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER, recipientPhoneNumber); err != nil {
logg.ErrorCtxf(ctx, "Failed to write recipient phone number", "type", txType, "error", err)
return err
}
} else {
logg.InfoCtxf(ctx, "No recipient phone number found for public key", "publicKey", string(publicKey))
}
return nil
} }
// TransactionReset resets the previous transaction data (Recipient and Amount) // TransactionReset resets the previous transaction data (Recipient and Amount)
@ -164,6 +268,16 @@ func (h *MenuHandlers) TransactionReset(ctx context.Context, sym string, input [
return res, nil return res, nil
} }
err = store.WriteEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE, []byte(""))
if err != nil {
return res, nil
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER, []byte(""))
if err != nil {
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_invalid_recipient, flag_invalid_recipient_with_invite) res.FlagReset = append(res.FlagReset, flag_invalid_recipient, flag_invalid_recipient_with_invite)
return res, nil return res, nil
@ -180,40 +294,223 @@ func (h *MenuHandlers) ResetTransactionAmount(ctx context.Context, sym string, i
} }
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount") flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction")
store := h.userdataStore store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte("")) err = store.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(""))
if err != nil { if err != nil {
return res, nil return res, nil
} }
res.FlagReset = append(res.FlagReset, flag_invalid_amount) res.FlagReset = append(res.FlagReset, flag_invalid_amount, flag_swap_transaction)
return res, nil return res, nil
} }
// MaxAmount gets the current balance from the API and sets it as // MaxAmount checks the transaction type to determine the displayed max amount.
// If the transaction type is "swap", it checks the max swappable amount and sets this as the content.
// If the transaction type is "normal", gets the current sender's balance from the store and sets it as
// the result content. // the result content.
func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *MenuHandlers) MaxAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore
activeBal, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL) flag_api_error, _ := h.flagManager.GetFlag("flag_api_error")
flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction")
userStore := h.userdataStore
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
// Fetch session data
transactionType, activeBal, activeSym, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId)
if err != nil { if err != nil {
logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", storedb.DATA_ACTIVE_BAL, "error", err)
return res, err return res, err
} }
res.Content = string(activeBal) // Format the active balance amount to 2 decimal places
formattedBalance, _ := store.TruncateDecimalString(string(activeBal), 2)
// If normal transaction, or if the sym is max_amount, return balance
if string(transactionType) == "normal" || sym == "max_amount" {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
res.Content = l.Get("Maximum amount: %s %s\nEnter amount:", formattedBalance, string(activeSym))
return res, nil
}
res.FlagSet = append(res.FlagSet, flag_swap_transaction)
// Get the recipient's phone number to read other data items
recipientPhoneNumber, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER)
if err != nil {
// invalid state
return res, err
}
recipientActiveSym, recipientActiveAddress, recipientActiveDecimal, err := h.getRecipientData(ctx, string(recipientPhoneNumber))
if err != nil {
return res, err
}
// Resolve active pool address
activePoolAddress, err := h.resolveActivePoolAddress(ctx, sessionId)
if err != nil {
return res, err
}
// Check if sender token is swappable
canSwap, err := h.accountService.CheckTokenInPool(ctx, string(activePoolAddress), string(activeAddress))
if err != nil || !canSwap.CanSwapFrom {
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed on CheckTokenInPool", "error", err)
}
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
res.Content = l.Get("Maximum amount: %s %s\nEnter amount:", formattedBalance, string(activeSym))
return res, nil
}
// retrieve the max credit send amounts
maxSAT, maxRAT, err := h.calculateSendCreditLimits(ctx, activePoolAddress, activeAddress, recipientActiveAddress, publicKey, activeDecimal, recipientActiveDecimal)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, err
}
// Fallback if below minimum
maxFloat, _ := strconv.ParseFloat(maxSAT, 64)
if maxFloat < 0.1 {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
res.Content = l.Get("Maximum amount: %s %s\nEnter amount:", formattedBalance, string(activeSym))
return res, nil
}
// Save max RAT amount to be used in validating the user's input
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, []byte(maxRAT))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write swap max amount (maxRAT)", "value", maxRAT, "error", err)
return res, err
}
// save swap related data for the swap preview
metadata := &dataserviceapi.TokenHoldings{
TokenAddress: string(recipientActiveAddress),
TokenSymbol: string(recipientActiveSym),
TokenDecimals: string(recipientActiveDecimal),
}
// Store the active swap_to data
if err := store.UpdateSwapToVoucherData(ctx, userStore, sessionId, metadata); err != nil {
logg.ErrorCtxf(ctx, "failed on UpdateSwapToVoucherData", "error", err)
return res, err
}
res.Content = l.Get(
"Credit Available: %s %s\n(You can swap up to %s %s -> %s %s).\nEnter %s amount:",
maxRAT,
string(recipientActiveSym),
maxSAT,
string(activeSym),
maxRAT,
string(recipientActiveSym),
string(recipientActiveSym),
)
return res, nil return res, nil
} }
func (h *MenuHandlers) getSessionData(ctx context.Context, sessionId string) (transactionType, activeBal, activeSym, activeAddress, publicKey, activeDecimal []byte, err error) {
store := h.userdataStore
transactionType, err = store.ReadEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE)
if err != nil {
return
}
activeBal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL)
if err != nil {
return
}
activeAddress, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
if err != nil {
return
}
activeSym, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM)
if err != nil {
return
}
publicKey, err = store.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
return
}
activeDecimal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_DECIMAL)
if err != nil {
return
}
return
}
func (h *MenuHandlers) getRecipientData(ctx context.Context, sessionId string) (recipientActiveSym, recipientActiveAddress, recipientActiveDecimal []byte, err error) {
store := h.userdataStore
recipientActiveSym, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM)
if err != nil {
return
}
recipientActiveAddress, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
if err != nil {
return
}
recipientActiveDecimal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_DECIMAL)
if err != nil {
return
}
return
}
func (h *MenuHandlers) resolveActivePoolAddress(ctx context.Context, sessionId string) ([]byte, error) {
store := h.userdataStore
addr, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS)
if err == nil {
return addr, nil
}
if db.IsNotFound(err) {
defaultAddr := []byte(config.DefaultPoolAddress())
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS, defaultAddr); err != nil {
logg.ErrorCtxf(ctx, "failed to write default pool address", "error", err)
return nil, err
}
return defaultAddr, nil
}
logg.ErrorCtxf(ctx, "failed to read active pool address", "error", err)
return nil, err
}
func (h *MenuHandlers) calculateSendCreditLimits(ctx context.Context, poolAddress, fromAddress, toAddress, publicKey, fromDecimal, toDecimal []byte) (string, string, error) {
creditSendMaxLimits, err := h.accountService.GetCreditSendMaxLimit(
ctx,
string(poolAddress),
string(fromAddress),
string(toAddress),
string(publicKey),
)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetCreditSendMaxLimit", "error", err)
return "", "", err
}
scaledSAT := store.ScaleDownBalance(creditSendMaxLimits.MaxSAT, string(fromDecimal))
formattedSAT, _ := store.TruncateDecimalString(string(scaledSAT), 2)
scaledRAT := store.ScaleDownBalance(creditSendMaxLimits.MaxRAT, string(toDecimal))
formattedRAT, _ := store.TruncateDecimalString(string(scaledRAT), 2)
return formattedSAT, formattedRAT, nil
}
// ValidateAmount ensures that the given input is a valid amount and that // ValidateAmount ensures that the given input is a valid amount and that
// it is not more than the current balance. // it is not more than the current balance.
func (h *MenuHandlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *MenuHandlers) ValidateAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
@ -285,7 +582,7 @@ func (h *MenuHandlers) GetRecipient(ctx context.Context, sym string, input []byt
recipient, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE) recipient, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if len(recipient) == 0 { if len(recipient) == 0 {
logg.ErrorCtxf(ctx, "recipient is empty", "key", storedb.DATA_TEMPORARY_VALUE) logg.ErrorCtxf(ctx, "recipient is empty", "key", storedb.DATA_TEMPORARY_VALUE)
return res, fmt.Errorf("Data error encountered") return res, fmt.Errorf("data error encountered")
} }
res.Content = string(recipient) res.Content = string(recipient)
@ -307,7 +604,7 @@ func (h *MenuHandlers) GetSender(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
// GetAmount retrieves the amount from teh Gdbm Db. // GetAmount retrieves the transaction amount from the store.
func (h *MenuHandlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *MenuHandlers) GetAmount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
@ -391,3 +688,214 @@ func (h *MenuHandlers) InitiateTransaction(ctx context.Context, sym string, inpu
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil return res, nil
} }
// TransactionSwapPreview displays the send swap preview and estimates
func (h *MenuHandlers) TransactionSwapPreview(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
// Input in RAT
inputStr := string(input)
if inputStr == "0" {
return res, nil
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
recipientPhoneNumber, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER)
if err != nil {
// invalid state
return res, err
}
swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId)
if err != nil {
return res, err
}
// use the stored max RAT
maxRATValue, err := strconv.ParseFloat(swapData.ActiveSwapMaxAmount, 64)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to convert the swapMaxAmount to a float", "error", err)
return res, err
}
inputAmount, err := strconv.ParseFloat(inputStr, 64)
if err != nil || inputAmount > maxRATValue {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
// Format the amount to 2 decimal places
formattedAmount, err := store.TruncateDecimalString(inputStr, 2)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
finalAmountStr, err := store.ParseAndScaleAmount(formattedAmount, swapData.ActiveSwapToDecimal)
if err != nil {
return res, err
}
// call the credit send API to get the reverse quote
r, err := h.accountService.GetCreditSendReverseQuote(ctx, swapData.ActivePoolAddress, swapData.ActiveSwapFromAddress, swapData.ActiveSwapToAddress, finalAmountStr)
if err != nil {
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed GetCreditSendReverseQuote poolSwap", "error", err)
return res, nil
}
sendInputAmount := r.InputAmount // amount of SAT that should be swapped
sendOutputAmount := r.OutputAmount // amount of RAT that will be received
// store the sendOutputAmount as the final amount (that will be sent)
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(sendOutputAmount))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write output amount value entry with", "key", storedb.DATA_AMOUNT, "value", sendOutputAmount, "error", err)
return res, err
}
// Scale down the quoted output amount
quoteAmountStr := store.ScaleDownBalance(sendOutputAmount, swapData.ActiveSwapToDecimal)
// Format the qouteAmount amount to 2 decimal places
qouteAmount, _ := store.TruncateDecimalString(quoteAmountStr, 2)
// store the qouteAmount in the temporary value
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(qouteAmount))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporary qouteAmount entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", qouteAmount, "error", err)
return res, err
}
// store the sendInputAmount as the swap amount
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_AMOUNT, []byte(sendInputAmount))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write swap amount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "value", sendInputAmount, "error", err)
return res, err
}
res.Content = l.Get(
"%s will receive %s %s",
string(recipientPhoneNumber), qouteAmount, swapData.ActiveSwapToSym,
)
return res, nil
}
// TransactionInitiateSwap calls the poolSwap and returns a confirmation based on the result.
func (h *MenuHandlers) TransactionInitiateSwap(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId)
if err != nil {
return res, err
}
swapAmount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_AMOUNT)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read swapAmount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "error", err)
return res, err
}
swapAmountStr := string(swapAmount)
// Call the poolSwap API
poolSwap, err := h.accountService.PoolSwap(ctx, swapAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress)
if err != nil {
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil
}
swapTrackingId := poolSwap.TrackingId
logg.InfoCtxf(ctx, "poolSwap", "swapTrackingId", swapTrackingId)
// Initiate a send
recipientPublicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read swapAmount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "error", err)
return res, err
}
recipientPhoneNumber, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER)
if err != nil {
// invalid state
return res, err
}
// read the amount that should be sent
amount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_AMOUNT)
if err != nil {
// invalid state
return res, err
}
// Call TokenTransfer with the expected swap amount
tokenTransfer, err := h.accountService.TokenTransfer(ctx, string(amount), swapData.PublicKey, string(recipientPublicKey), swapData.ActiveSwapToAddress)
if err != nil {
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on TokenTransfer", "error", err)
return res, nil
}
trackingId := tokenTransfer.TrackingId
logg.InfoCtxf(ctx, "TokenTransfer", "trackingId", trackingId)
res.Content = l.Get(
"Your request has been sent. %s will receive %s %s from %s.",
string(recipientPhoneNumber),
swapData.TemporaryValue,
swapData.ActiveSwapToSym,
sessionId,
)
res.FlagReset = append(res.FlagReset, flag_account_authorized, flag_swap_transaction)
return res, nil
}
// ClearTransactionTypeFlag resets the flag when a user goes back.
func (h *MenuHandlers) ClearTransactionTypeFlag(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_swap_transaction, _ := h.flagManager.GetFlag("flag_swap_transaction")
inputStr := string(input)
if inputStr == "0" {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
return res, nil
}
return res, nil
}

View File

@ -86,6 +86,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService)
ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset) ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset)
ls.DbRs.AddLocalFunc("invite_valid_recipient", appHandlers.InviteValidRecipient) ls.DbRs.AddLocalFunc("invite_valid_recipient", appHandlers.InviteValidRecipient)
ls.DbRs.AddLocalFunc("max_amount", appHandlers.MaxAmount) ls.DbRs.AddLocalFunc("max_amount", appHandlers.MaxAmount)
ls.DbRs.AddLocalFunc("credit_max_amount", appHandlers.MaxAmount)
ls.DbRs.AddLocalFunc("validate_amount", appHandlers.ValidateAmount) ls.DbRs.AddLocalFunc("validate_amount", appHandlers.ValidateAmount)
ls.DbRs.AddLocalFunc("reset_transaction_amount", appHandlers.ResetTransactionAmount) ls.DbRs.AddLocalFunc("reset_transaction_amount", appHandlers.ResetTransactionAmount)
ls.DbRs.AddLocalFunc("get_recipient", appHandlers.GetRecipient) ls.DbRs.AddLocalFunc("get_recipient", appHandlers.GetRecipient)
@ -136,6 +137,10 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService)
ls.DbRs.AddLocalFunc("swap_max_limit", appHandlers.SwapMaxLimit) ls.DbRs.AddLocalFunc("swap_max_limit", appHandlers.SwapMaxLimit)
ls.DbRs.AddLocalFunc("swap_preview", appHandlers.SwapPreview) ls.DbRs.AddLocalFunc("swap_preview", appHandlers.SwapPreview)
ls.DbRs.AddLocalFunc("initiate_swap", appHandlers.InitiateSwap) ls.DbRs.AddLocalFunc("initiate_swap", appHandlers.InitiateSwap)
ls.DbRs.AddLocalFunc("transaction_swap_preview", appHandlers.TransactionSwapPreview)
ls.DbRs.AddLocalFunc("transaction_initiate_swap", appHandlers.TransactionInitiateSwap)
ls.DbRs.AddLocalFunc("clear_trans_type_flag", appHandlers.ClearTransactionTypeFlag)
ls.first = appHandlers.Init ls.first = appHandlers.Init
return appHandlers, nil return appHandlers, nil

View File

@ -5,19 +5,19 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "2", "input": "3",
"expectedContent": "My vouchers\n1:Select voucher\n2:Voucher details\n0:Back" "expectedContent": "My vouchers\n1:Select voucher\n2:Voucher details\n0:Back"
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Select number or symbol from your vouchers:\n1SRF\n0:Back\n99:Quit" "expectedContent": "Select number or symbol from your vouchers:\n1:SRF\n0:Back\n99:Quit"
}, },
{ {
"input": "", "input": "",
"expectedContent": "Select number or symbol from your vouchers:\n1SRF\n0:Back\n99:Quit" "expectedContent": "Select number or symbol from your vouchers:\n1:SRF\n0:Back\n99:Quit"
}, },
{ {
"input": "1", "input": "1",
@ -29,7 +29,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -38,15 +38,15 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "2", "input": "3",
"expectedContent": "My vouchers\n1:Select voucher\n2:Voucher details\n0:Back" "expectedContent": "My vouchers\n1:Select voucher\n2:Voucher details\n0:Back"
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Select number or symbol from your vouchers:\n1SRF\n0:Back\n99:Quit" "expectedContent": "Select number or symbol from your vouchers:\n1:SRF\n0:Back\n99:Quit"
}, },
{ {
"input": "SRF", "input": "SRF",
@ -58,7 +58,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -67,10 +67,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -95,7 +95,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -104,10 +104,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -136,7 +136,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -145,10 +145,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -177,7 +177,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -186,10 +186,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -210,7 +210,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -219,10 +219,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -235,7 +235,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -244,10 +244,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -280,7 +280,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -289,10 +289,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -325,7 +325,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -334,10 +334,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -382,7 +382,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -391,10 +391,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -419,7 +419,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -428,10 +428,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -460,7 +460,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -469,10 +469,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -497,7 +497,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -506,10 +506,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -534,7 +534,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -543,10 +543,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -571,7 +571,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -580,10 +580,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -604,7 +604,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
}, },
@ -613,10 +613,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {

View File

@ -5,10 +5,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -49,7 +49,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
} }

View File

@ -5,10 +5,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -53,7 +53,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
} }

View File

@ -5,10 +5,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -45,7 +45,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
} }

View File

@ -5,10 +5,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -37,7 +37,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
} }

View File

@ -5,10 +5,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -33,7 +33,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
} }

View File

@ -5,10 +5,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -41,7 +41,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
} }

View File

@ -5,10 +5,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {
@ -61,7 +61,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
} }
] ]
} }

View File

@ -57,7 +57,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
@ -86,10 +86,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "4", "input": "6",
"expectedContent": "For more help,please call: 0757628885" "expectedContent": "For more help,please call: 0757628885"
} }
] ]
@ -99,7 +99,7 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "9", "input": "9",
@ -112,10 +112,10 @@
"steps": [ "steps": [
{ {
"input": "", "input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" "expectedContent": "{balance}\n\n1:Send\n2:Swap\n3:My Vouchers\n4:Select pool\n5:My Account\n6:Help\n9:Quit"
}, },
{ {
"input": "3", "input": "5",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back" "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n7:My Alias\n0:Back"
}, },
{ {

View File

@ -1,2 +1 @@
Maximum amount: {{.max_amount}} {{.max_amount}}
Enter amount:

View File

@ -1,5 +1,5 @@
LOAD reset_transaction_amount 0 LOAD reset_transaction_amount 10
LOAD max_amount 40 LOAD max_amount 160
RELOAD max_amount RELOAD max_amount
MAP max_amount MAP max_amount
MOUT back 0 MOUT back 0

View File

@ -1,2 +1 @@
Kiwango cha juu: {{.max_amount}} {{.max_amount}}
Weka kiwango:

View File

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

View File

@ -0,0 +1,18 @@
LOAD reset_transaction_amount 10
LOAD credit_max_amount 160
RELOAD credit_max_amount
MAP credit_max_amount
MOUT back 0
HALT
LOAD clear_trans_type_flag 6
RELOAD clear_trans_type_flag
CATCH transaction_swap flag_swap_transaction 1
LOAD validate_amount 64
RELOAD validate_amount
CATCH api_failure flag_api_call_error 1
CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0
LOAD get_recipient 0
LOAD get_sender 64
LOAD get_amount 32
INCMP transaction_pin *

View File

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

View File

@ -0,0 +1 @@
Enter recipient's phone number/address/alias:

View File

@ -0,0 +1,12 @@
LOAD transaction_reset 0
RELOAD transaction_reset
CATCH no_voucher flag_no_active_voucher 1
MOUT back 0
HALT
LOAD validate_recipient 50
RELOAD validate_recipient
CATCH api_failure flag_api_call_error 1
CATCH invalid_recipient flag_invalid_recipient 1
CATCH invite_recipient flag_invalid_recipient_with_invite 1
INCMP _ 0
INCMP credit_amount *

View File

@ -0,0 +1 @@
Credit-Send

View File

@ -0,0 +1 @@
Tuma-Mkopo

View File

@ -0,0 +1 @@
Weka nambari ya simu/anwani/lakabu:

View File

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

View File

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

View File

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

View File

@ -45,3 +45,12 @@ msgstr "Jina: %s\nSarafu: %s"
msgid "Only USD vouchers are allowed to mpesa.sarafu.eth." msgid "Only USD vouchers are allowed to mpesa.sarafu.eth."
msgstr "Ni sarafu za USD pekee zinazoruhusiwa kwa mpesa.sarafu.eth." msgstr "Ni sarafu za USD pekee zinazoruhusiwa kwa mpesa.sarafu.eth."
msgid "Maximum amount: %s %s\nEnter amount:"
msgstr "Kiwango cha juu: %s %s\nWeka kiwango:"
msgid "Credit Available: %s %s\n(You can swap up to %s %s -> %s %s).\nEnter %s amount:"
msgstr "Kiwango kinachopatikana: %s %s\n(Unaweza kubadilisha hadi %s %s -> %s %s)\nWeka kiwango cha %s:"
msgid "%s will receive %s %s"
msgstr "%s atapokea %s %s"

View File

@ -7,18 +7,20 @@ LOAD check_balance 128
RELOAD check_balance RELOAD check_balance
MAP check_balance MAP check_balance
MOUT send 1 MOUT send 1
MOUT swap 2 MOUT credit_send 2
MOUT vouchers 3 MOUT swap 3
MOUT select_pool 4 MOUT vouchers 4
MOUT account 5 MOUT select_pool 5
MOUT help 6 MOUT account 6
MOUT help 7
MOUT quit 9 MOUT quit 9
HALT HALT
INCMP send 1 INCMP send 1
INCMP swap_to_list 2 INCMP credit_send 2
INCMP my_vouchers 3 INCMP swap_to_list 3
INCMP select_pool 4 INCMP my_vouchers 4
INCMP my_account 5 INCMP select_pool 5
INCMP help 6 INCMP my_account 6
INCMP help 7
INCMP quit 9 INCMP quit 9
INCMP . * INCMP . *

View File

@ -35,3 +35,4 @@ flag,flag_account_pin_reset,41,this is set on an account when an admin triggers
flag,flag_incorrect_pool,42,this is set when the user selects an invalid pool flag,flag_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_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_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

Can't render this file because it has a wrong number of fields in line 34.

View File

@ -1,5 +1,6 @@
CATCH no_voucher flag_no_active_voucher 1 CATCH no_voucher flag_no_active_voucher 1
LOAD get_pools 0 LOAD get_pools 0
RELOAD get_pools
MAP get_pools MAP get_pools
LOAD get_default_pool 20 LOAD get_default_pool 20
RELOAD get_default_pool RELOAD get_default_pool

View File

@ -1 +1 @@
Weka nambari ya simu: Weka nambari ya simu/Anwani/Lakabu:

View File

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

View File

@ -0,0 +1,13 @@
LOAD transaction_swap_preview 0
MAP transaction_swap_preview
CATCH api_failure flag_api_call_error 1
CATCH invalid_credit_send_amount flag_invalid_amount 1
MOUT back 0
MOUT quit 9
LOAD authorize_account 6
HALT
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
INCMP _ 0
INCMP quit 9
INCMP transaction_swap_initiated *

View File

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

View File

@ -0,0 +1,3 @@
{{.transaction_swap_preview}}
Tafadhali weka PIN yako kudhibitisha:

View File

@ -89,6 +89,10 @@ const (
DATA_ACTIVE_POOL_NAME DATA_ACTIVE_POOL_NAME
// Holds the active pool symbol for the swap // Holds the active pool symbol for the swap
DATA_ACTIVE_POOL_SYM DATA_ACTIVE_POOL_SYM
// Holds the send transaction type
DATA_SEND_TRANSACTION_TYPE
// Holds the recipient formatted phone number
DATA_RECIPIENT_PHONE_NUMBER
) )
const ( const (

View File

@ -173,9 +173,9 @@ func UpdateSwapToVoucherData(ctx context.Context, store DataStore, sessionId str
logg.InfoCtxf(ctx, "UpdateSwapToVoucherData", "data", data) logg.InfoCtxf(ctx, "UpdateSwapToVoucherData", "data", data)
// Active swap to voucher data entries // Active swap to voucher data entries
activeEntries := map[storedb.DataTyp][]byte{ activeEntries := map[storedb.DataTyp][]byte{
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: []byte(data.TokenAddress),
storedb.DATA_ACTIVE_SWAP_TO_SYM: []byte(data.TokenSymbol), storedb.DATA_ACTIVE_SWAP_TO_SYM: []byte(data.TokenSymbol),
storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: []byte(data.TokenDecimals), storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: []byte(data.TokenDecimals),
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: []byte(data.TokenAddress),
} }
// Write active data // Write active data

View File

@ -14,13 +14,13 @@ func TestReadSwapData(t *testing.T) {
// Test swap data // Test swap data
swapData := map[storedb.DataTyp]string{ swapData := map[storedb.DataTyp]string{
storedb.DATA_PUBLIC_KEY: publicKey, storedb.DATA_PUBLIC_KEY: publicKey,
storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e", storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e",
storedb.DATA_ACTIVE_SWAP_FROM_SYM: "AMANI", storedb.DATA_ACTIVE_SYM: "AMANI",
storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: "6", storedb.DATA_ACTIVE_DECIMAL: "6",
storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", storedb.DATA_ACTIVE_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe",
storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD", storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD",
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a", storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
} }
// Store the data // Store the data
@ -53,15 +53,16 @@ func TestReadSwapPreviewData(t *testing.T) {
// Test swap preview data // Test swap preview data
swapPreviewData := map[storedb.DataTyp]string{ swapPreviewData := map[storedb.DataTyp]string{
storedb.DATA_PUBLIC_KEY: publicKey, storedb.DATA_TEMPORARY_VALUE: "temp",
storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT: "1339482", storedb.DATA_PUBLIC_KEY: publicKey,
storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: "6", storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT: "1339482",
storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e", storedb.DATA_ACTIVE_DECIMAL: "6",
storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e",
storedb.DATA_ACTIVE_SWAP_FROM_SYM: "AMANI", storedb.DATA_ACTIVE_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe",
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a", storedb.DATA_ACTIVE_SYM: "AMANI",
storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD", storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: "18", storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD",
storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: "18",
} }
// Store the data // Store the data
@ -72,6 +73,7 @@ func TestReadSwapPreviewData(t *testing.T) {
} }
expectedResult := SwapPreviewData{ expectedResult := SwapPreviewData{
TemporaryValue: "temp",
PublicKey: "0X13242618721", PublicKey: "0X13242618721",
ActiveSwapMaxAmount: "1339482", ActiveSwapMaxAmount: "1339482",
ActiveSwapFromDecimal: "6", ActiveSwapFromDecimal: "6",

View File

@ -7,6 +7,7 @@ import (
"math/big" "math/big"
"reflect" "reflect"
"strconv" "strconv"
"strings"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
) )
@ -21,25 +22,34 @@ type TransactionData struct {
ActiveAddress string ActiveAddress string
} }
// TruncateDecimalString safely truncates the input amount to the specified decimal places // TruncateDecimalString safely truncates (not rounds) a number string to the specified decimal places
func TruncateDecimalString(input string, decimalPlaces int) (string, error) { func TruncateDecimalString(input string, decimalPlaces int) (string, error) {
num, ok := new(big.Float).SetString(input) if _, err := strconv.ParseFloat(input, 64); err != nil {
if !ok {
return "", fmt.Errorf("invalid input") return "", fmt.Errorf("invalid input")
} }
// Multiply by 10^decimalPlaces // Split input into integer and fractional parts
scale := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimalPlaces)), nil)) parts := strings.SplitN(input, ".", 2)
scaled := new(big.Float).Mul(num, scale) intPart := parts[0]
var fracPart string
// Truncate by converting to int (chops off decimals) if len(parts) == 2 {
intPart, _ := scaled.Int(nil) fracPart = parts[1]
}
// Divide back to get truncated float // Truncate or pad fractional part
truncated := new(big.Float).Quo(new(big.Float).SetInt(intPart), scale) if len(fracPart) > decimalPlaces {
fracPart = fracPart[:decimalPlaces]
} else {
fracPart = fracPart + strings.Repeat("0", decimalPlaces-len(fracPart))
}
// Format with fixed decimals // Handle zero decimal places
return truncated.Text('f', decimalPlaces), nil if decimalPlaces == 0 {
return intPart, nil
}
return fmt.Sprintf("%s.%s", intPart, fracPart), nil
} }
func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) { func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) {

View File

@ -22,6 +22,13 @@ func TestTruncateDecimalString(t *testing.T) {
want: "4.00", want: "4.00",
expectError: false, expectError: false,
}, },
{
name: "precision test",
input: "2.1",
decimalPlaces: 2,
want: "2.10",
expectError: false,
},
{ {
name: "single decimal", name: "single decimal",
input: "4.1", input: "4.1",