Compare commits

...

107 Commits

Author SHA1 Message Date
89d3d19d77 Merge pull request 'use the ResetRoot config to clear the cache once a user quits' (#112) from cache-error-fix into master
Reviewed-on: #112
2025-12-09 10:29:37 +01:00
817b523135 Merge branch 'master' into cache-error-fix
Some checks failed
release / docker (push) Has been cancelled
2025-12-08 17:43:21 +03:00
e0e3d9b6cf Merge pull request 'mpesa-onramp-offramp' (#110) from mpesa-onramp-offramp into master
Reviewed-on: #110
2025-12-08 15:41:23 +01:00
2cabae1e74
use the ResetRoot config to clear the cache once a user quits
Some checks are pending
release / docker (push) Waiting to run
2025-12-08 17:29:13 +03:00
f949b83a51
change the go-vise source
Some checks failed
release / docker (push) Has been cancelled
2025-12-03 13:40:32 +03:00
d586c41cca
add sleep for 1 second between requests
Some checks failed
release / docker (push) Has been cancelled
2025-12-03 13:12:24 +03:00
518baceee5
update the translations to add the approximation sign ~
Some checks failed
release / docker (push) Has been cancelled
2025-12-02 12:28:55 +03:00
1cb82e9099
call the mpesa rates API to get the rates 2025-12-02 12:28:28 +03:00
efc93397b2
use the updated sarafu-api 2025-12-02 12:26:52 +03:00
5b19b3409b
remove the mpesa rates configs 2025-12-02 12:26:36 +03:00
2db97cde81
remove the hard-coded rates 2025-12-02 12:26:13 +03:00
3ba891a184
update docker to use go 1.24
Some checks failed
release / docker (push) Has been cancelled
2025-11-28 13:04:15 +03:00
7877534366
prevent users without a voucher from accessing the Get M-Pesa menu
Some checks are pending
release / docker (push) Waiting to run
2025-11-28 12:01:17 +03:00
e658460845 Merge branch 'master' into mpesa-onramp-offramp 2025-11-28 11:54:29 +03:00
025c32c5f0 Merge pull request 'clear the current active voucher when no vouchers exist' (#107) from update-balance-on-empty-vouchers into master
Reviewed-on: #107
2025-11-28 09:53:21 +01:00
bbc5b8f82a Merge branch 'master' into update-balance-on-empty-vouchers 2025-11-28 11:42:40 +03:00
b8829e917b Merge pull request 'cached_symbols_hotfix' (#111) from cached_symbols_hotfix into master
Reviewed-on: #111
2025-11-28 09:21:39 +01:00
17c9925b14
convert the amount to an int before calling the MpesaTriggerOnramp API 2025-11-28 10:40:43 +03:00
2b475a78c9
update the sarafu-api package 2025-11-28 10:40:11 +03:00
ac29ee8e26
added Mpesa related env variables 2025-11-28 09:31:52 +03:00
9b8c5a021b
added the send mpesa functionality with the use of config values 2025-11-27 16:45:56 +03:00
0da64b8565
added the send_mpesa symbols 2025-11-27 16:44:33 +03:00
04293d5476
added the final send mpesa node to initiate the transaction 2025-11-27 16:44:09 +03:00
15bf7dc956
added a node for the send mpesa confirmation and PIN input 2025-11-27 16:43:34 +03:00
0fd1f43602
added a node for invalid send mpesa amounts 2025-11-27 16:42:37 +03:00
8433bda6f6
updated the naming to 'M-Pesa' 2025-11-27 16:41:43 +03:00
010696e153
added the send_mpesa node 2025-11-27 16:41:11 +03:00
a5cdd72480
added M-Pesa related translations 2025-11-27 16:38:58 +03:00
06d6ab8692
added back navigation 2025-11-27 16:37:57 +03:00
45a6ef4066
added Mpesa related configs and variables 2025-11-27 16:37:31 +03:00
7ae4a6fd5d
updated the common package for FormatToLocalPhoneNumber 2025-11-27 16:36:50 +03:00
98bc2dbac1
refactored the code for proper transaction flow for normal and swap transfers 2025-11-27 13:33:00 +03:00
a438697e25
change the order of INCMP statements for better flow 2025-11-27 11:55:59 +03:00
c610f0c9c1
added the get mpesa symbols 2025-11-26 18:02:50 +03:00
edaf527aa1
added the get mpesa functionality 2025-11-26 18:02:28 +03:00
c4026151c0
properly format the flags 2025-11-26 18:01:33 +03:00
f2a8dc3a80
added the get mpesa translations 2025-11-26 18:01:03 +03:00
fe168f8476
added the initiate get mpesa node 2025-11-26 18:00:40 +03:00
c8f081c833
added the get mpesa confirmation node 2025-11-26 17:59:26 +03:00
08d0043d2c
added the get mpesa menu node 2025-11-26 17:58:36 +03:00
0d9d4c67ce
added the mpesa address config access 2025-11-26 17:53:08 +03:00
0eec10278a
added the default mpesa address env variable 2025-11-26 17:52:45 +03:00
398610924b
removed the debug output
Some checks failed
release / docker (push) Has been cancelled
2025-11-26 13:02:26 +03:00
3fdb0e3426
update the symbol to send_max_amount with debug output 2025-11-26 12:56:04 +03:00
f7a2958ba2
added the mpesa node with get and send mpesa 2025-11-24 14:29:59 +03:00
d16d726ce7
added the top level Mpesa node 2025-11-24 10:37:13 +03:00
c18d40286e Merge pull request 'credit-send-hotfix' (#109) from credit-send-hotfix into master
Reviewed-on: #109
2025-11-18 08:52:21 +01:00
c2cf8e91e5
removed the print debug statements
Some checks failed
release / docker (push) Has been cancelled
2025-11-14 10:55:54 +03:00
20cfb47940
updated the size outputs 2025-11-14 10:49:11 +03:00
03d6cbd429
added a debug for the returned content 2025-11-14 10:46:22 +03:00
ee41f73347
added a debug log 2025-11-14 10:41:34 +03:00
bafe3cc04e
added the swap menu swahili version 2025-11-13 17:13:34 +03:00
308dc93a91
return nil to CATCH the flag_api_call_error 2025-11-13 17:09:30 +03:00
8d29c364c3
updated the swahili message for output length error 2025-11-13 17:07:57 +03:00
d52d6c478f
added a CATCH for the api_call_error flag
Some checks failed
release / docker (push) Has been cancelled
2025-11-13 01:46:36 +03:00
208eac5a3c
use the correct 'flag_api_call_error' 2025-11-13 01:44:50 +03:00
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
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
6811cc12ec
clear the current active voucher data if it exists when no vouchers exist 2025-10-23 10:29:14 +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
76 changed files with 1808 additions and 474 deletions

View File

@ -29,3 +29,11 @@ DEFAULT_POOL_CONTRACT_ADDRESS=0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e
DEFAULT_LIMITER_ADDRESS= DEFAULT_LIMITER_ADDRESS=
DEFAULT_VOUCHER_REGISTRY= DEFAULT_VOUCHER_REGISTRY=
INCLUDE_STABLES_PARAM=false INCLUDE_STABLES_PARAM=false
#Mpesa
DEFAULT_MPESA_ADDRESS=0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e
MIN_MPESA_SEND_AMOUNT=100
MAX_MPESA_SEND_AMOUNT=250000
DEFAULT_MPESA_ASSET=cUSD
MPESA_BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
MPESA_ONRAMP_BASE=https://pretium.v1.grassecon.net

View File

@ -1,4 +1,4 @@
FROM golang:1.23.4-bookworm AS build FROM golang:1.24-bookworm AS build
ENV CGO_ENABLED=1 ENV CGO_ENABLED=1
@ -11,7 +11,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libgdbm-dev \ libgdbm-dev \
git \ git \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN git clone https://git.defalsify.org/vise.git go-vise RUN git clone https://github.com/nolash/go-vise go-vise
COPY . ./sarafu-vise COPY . ./sarafu-vise
WORKDIR /build/sarafu-vise/services/registration WORKDIR /build/sarafu-vise/services/registration

View File

@ -84,6 +84,7 @@ func main() {
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator, MenuSeparator: menuSeparator,
ResetOnEmptyInput: true, ResetOnEmptyInput: true,
ResetRoot: true, // clear the cache once a user quits
} }
if engineDebug { if engineDebug {

View File

@ -99,6 +99,7 @@ func main() {
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator, MenuSeparator: menuSeparator,
ResetOnEmptyInput: true, ResetOnEmptyInput: true,
ResetRoot: true, // clear the cache once a user quits
} }
if engineDebug { if engineDebug {

View File

@ -82,6 +82,7 @@ func main() {
MenuSeparator: menuSeparator, MenuSeparator: menuSeparator,
EngineDebug: engineDebug, EngineDebug: engineDebug,
ResetOnEmptyInput: true, ResetOnEmptyInput: true,
ResetRoot: true, // clear the cache once a user quits
} }
menuStorageService := storage.NewMenuStorageService(conns) menuStorageService := storage.NewMenuStorageService(conns)

View File

@ -1,6 +1,7 @@
package config package config
import ( import (
"strconv"
"strings" "strings"
apiconfig "git.grassecon.net/grassrootseconomics/sarafu-api/config" apiconfig "git.grassecon.net/grassrootseconomics/sarafu-api/config"
@ -87,3 +88,29 @@ func DefaultPoolName() string {
func DefaultPoolSymbol() string { func DefaultPoolSymbol() string {
return env.GetEnv("DEFAULT_POOL_SYMBOL", "") return env.GetEnv("DEFAULT_POOL_SYMBOL", "")
} }
func DefaultMpesaAddress() string {
return env.GetEnv("DEFAULT_MPESA_ADDRESS", "")
}
func MinMpesaSendAmount() float64 {
v := env.GetEnv("MIN_MPESA_SEND_AMOUNT", "100")
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return 100 // fallback
}
return f
}
func MaxMpesaSendAmount() float64 {
v := env.GetEnv("MAX_MPESA_SEND_AMOUNT", "250000")
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return 250000 // fallback
}
return f
}
func DefaultMpesaAsset() string {
return env.GetEnv("DEFAULT_MPESA_ASSET", "")
}

74
go.mod
View File

@ -1,68 +1,66 @@
module git.grassecon.net/grassrootseconomics/sarafu-vise module git.grassecon.net/grassrootseconomics/sarafu-vise
go 1.23.4 go 1.24.0
toolchain go1.24.10
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.20251127132814-8ceadabbc215
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.20251202085112-45469d4ba326
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
github.com/gofrs/uuid v4.4.0+incompatible github.com/gofrs/uuid v4.4.0+incompatible
github.com/grassrootseconomics/ethutils v1.3.1 github.com/grassrootseconomics/ethutils v1.6.0
github.com/grassrootseconomics/ussd-data-service v1.6.0-beta github.com/grassrootseconomics/ussd-data-service v1.10.1-beta
github.com/jackc/pgx/v5 v5.7.1 github.com/jackc/pgx/v5 v5.7.6
github.com/peteole/testdata-loader v0.3.0 github.com/peteole/testdata-loader v0.3.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.32.0 golang.org/x/crypto v0.45.0
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 gopkg.in/leonelquinteros/gotext.v1 v1.3.1
) )
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7 // indirect
github.com/alecthomas/participle/v2 v2.0.0 // indirect github.com/alecthomas/participle/v2 v2.0.0 // indirect
github.com/alecthomas/repr v0.2.0 // indirect github.com/alecthomas/repr v0.2.0 // indirect
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect github.com/barbashov/iso639-3 v1.0.0 // indirect
github.com/bits-and-blooms/bitset v1.14.3 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/consensys/gnark-crypto v0.19.2 // indirect
github.com/consensys/bavard v0.1.13 // indirect github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
github.com/ethereum/go-ethereum v1.14.9 // indirect github.com/ethereum/go-ethereum v1.16.7 // indirect
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/grassrootseconomics/eth-custodial v1.3.0-beta // indirect github.com/grassrootseconomics/eth-custodial v1.12.0-rc // indirect
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/holiman/uint256 v1.3.1 // indirect github.com/holiman/uint256 v1.3.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect github.com/joho/godotenv v1.5.1 // indirect
github.com/lmittmann/w3 v0.17.1 // indirect github.com/lmittmann/w3 v0.20.5 // indirect
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.3 // indirect
github.com/supranational/blst v0.3.11 // indirect github.com/supranational/blst v0.3.16 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
) )

260
go.sum
View File

@ -1,227 +1,167 @@
git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66 h1:hmtb2Q3lHxq+SXqG+Gn43pKhTRYx+sw5j1LpgBfXN1o= git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66 h1:hmtb2Q3lHxq+SXqG+Gn43pKhTRYx+sw5j1LpgBfXN1o=
git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20250417111317-2953f4c2f32e h1:DcC9qkNl9ny3hxQmsMK6W81+5R/j4ZwYUbvewMI/rlc= git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20251127132814-8ceadabbc215 h1:cxWmd3WG3iVEqP6qG8ZeQRa7Ujno3rSKz3YXjZnmTEY=
git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20250417111317-2953f4c2f32e/go.mod h1:wgQJZGIS6QuNLHqDhcsvehsbn5PvgV7aziRebMnJi60= git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20251127132814-8ceadabbc215/go.mod h1:wgQJZGIS6QuNLHqDhcsvehsbn5PvgV7aziRebMnJi60=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250623063234-c1797e7a32b5 h1:VnRe01kHkZUBK/QjE7iV6gElSqSwQnAkWV3yCHtuYrI= git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251128071248-bfdeef125576 h1:Ov4zENfEnzuU4ZpsNGbFjog9NUM0h1A7RYwWkmHRJWo=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250623063234-c1797e7a32b5/go.mod h1:H97hR+VOnZvR5BiGVb0ScCPwH/IoKBOlKM+yrQNVpq0= 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.20250623070026-d945964b0b46 h1:0+XkSRe7XSHa9WHXKpGPuC0myDszjchr4syH006lQ28= 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.20250623070026-d945964b0b46/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8= 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.20250623075057-7b42d509e6d4 h1:W+8CC7x5eCPylkGy2TEoOpfJuiIlqzEzyYTzCLlY/u8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250623075057-7b42d509e6d4/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250624074830-5aa032400c12 h1:vD8biQmN36eouuE+TdxgXQjKisRf5cTGu/tMPv3afs0=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250624074830-5aa032400c12/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250624090744-339ba854c997 h1:8bCKyYoV4YiVBvCZlRclc3aQlBYpWhgtM35mvniDFD8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250624090744-339ba854c997/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250626065419-57ee409f9629 h1:ew3vCFrLS/7/8uULTTPCbsHzFntQ6X68SScnBEy3pl0=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250626065419-57ee409f9629/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630213135-50ee455e7069 h1:re+hdr5NAC6JqhyvjMCkgX17fFi0u3Mawc6RBnBJW8I=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630213135-50ee455e7069/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630213606-12940bb5f284 h1:2zMU9jPd6xEO6oY9oxr84sdT9G3d09eyAkjVBAz9eco=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630213606-12940bb5f284/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630214912-814bef2b209a h1:KuhJ/WY4RCGmrXUA680ciaponM4vM5zBOJfnCpUo2fc=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250630214912-814bef2b209a/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251021120522-6f7802b58cf5 h1:bQglHVxMilciZ9M2PGuLgA+Wkvqo8OqQh6TFYwjtuSE=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251021120522-6f7802b58cf5/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63 h1:yznaUXeAy+qiZb2nCxosYXE5HyCPpynIoplEuYV/zQM=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251022084613-532547899f63/go.mod h1:y/vsN8UO0wSxZk3gv0y5df4RPKMJI6TIxjVcVCPF8T8=
git.grassecon.net/grassrootseconomics/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=
git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694/go.mod h1:DpibtYpnT3nG4Kn556hRAkdu4+CtiI/6MbnQHal51mQ= git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694/go.mod h1:DpibtYpnT3nG4Kn556hRAkdu4+CtiI/6MbnQHal51mQ=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7 h1:uups37roJCTtR/BrJa0WoMrxt3rzgV+Qrj+TxYyJoAo=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251119083800-2aa1d4cc79d7/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y= github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE= github.com/barbashov/iso639-3 v1.0.0 h1:qCp1hUzZT8C8yHcdDo4sZQg2jHEaX6LF5H/dF9ba0qs=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U= github.com/barbashov/iso639-3 v1.0.0/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.14.3 h1:Gd2c8lSNf9pKXom5JtD7AaKO8o7fGQ2LtFj1436qilA=
github.com/bits-and-blooms/bitset v1.14.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA=
github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ=
github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI=
github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M=
github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY=
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I=
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs=
github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI=
github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
github.com/ethereum/go-ethereum v1.14.9 h1:J7iwXDrtUyE9FUjUYbd4c9tyzwMh6dTJsKzo9i6SrwA= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s=
github.com/ethereum/go-ethereum v1.14.9/go.mod h1:QeW+MtTpRdBEm2pUFoonByee8zfHv7kGp0wK0odvU1I= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grassrootseconomics/eth-custodial v1.12.0-rc h1:INkXMfNUISkQ1bgnw3LwfFJn/eCdBawFU8Hx+S7mM/c=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU= github.com/grassrootseconomics/eth-custodial v1.12.0-rc/go.mod h1:s/vacSWFiVO7FDv76EID8sQvdT1vq+3GcWZUgU6N618=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= github.com/grassrootseconomics/ethutils v1.6.0 h1:Zx7Nqi9ACxkpsHLmWWj8AHGCGeBmjP8Qf79+YMTGLTY=
github.com/grassrootseconomics/ethutils v1.3.1 h1:LlQO90HjJkl7ejC8fv6jP7RJUrAm1j4VMMCYfsoIrhU= github.com/grassrootseconomics/ethutils v1.6.0/go.mod h1:7RCNwDj1wfgWt8CV4p7cQuZWPPI5fjwl/piq7NthaWg=
github.com/grassrootseconomics/ethutils v1.3.1/go.mod h1:Wuv1VEZrkLIXqTSEYI3Nh9HG/ZHOUQ+U+xvWJ8QtjgQ= github.com/grassrootseconomics/ussd-data-service v1.10.1-beta h1:ZrRn7gv4MVSg67hmSdiii/Y86PUIvhxtqc2luAjlA7o=
github.com/grassrootseconomics/ussd-data-service v1.5.0-beta h1:BSSQL/yPEvTVku9ja/ENZyZdwZkEaiTzzOUfg72bTy4= github.com/grassrootseconomics/ussd-data-service v1.10.1-beta/go.mod h1:Uigrnnwj0vaTz2az3VwHmAG7xpJgQWYTcLudLh6Zc1I=
github.com/grassrootseconomics/ussd-data-service v1.5.0-beta/go.mod h1:9sGnorpKaK76FmOGXoh/xv7x5siSFNYdXxQo9BKW4DI=
github.com/grassrootseconomics/ussd-data-service v1.6.0-beta h1:pY6zns6ifXyClRxP+JJaWrged3oRE7tTS2xaftF9clA=
github.com/grassrootseconomics/ussd-data-service v1.6.0-beta/go.mod h1:9sGnorpKaK76FmOGXoh/xv7x5siSFNYdXxQo9BKW4DI=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/lmittmann/w3 v0.17.1 h1:zdXIimmNmYfqOFur+Jqc9Yqwtq6jwnsQufbTOnSAtW4= github.com/lmittmann/w3 v0.20.5 h1:JN9eUaVifhnnEP5cdYpIvhvC6x2hUn9+LcWyPKLl8DI=
github.com/lmittmann/w3 v0.17.1/go.mod h1:WVUGMbL83WYBu4Sge3SVlW3qIG4VaHe+S8+UUnwz9Eg= github.com/lmittmann/w3 v0.20.5/go.mod h1:N/QfdxE9A8PpdX37AVJ4NjCdG9L5ZSuhqrru01ieRpM=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY=
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s= github.com/pashagolub/pgxmock/v4 v4.7.0 h1:de2ORuFYyjwOQR7NBm57+321RnZxpYiuUjsmqRiqgh8=
github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= github.com/pashagolub/pgxmock/v4 v4.7.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM=
github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I= github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I=
github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U= github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -232,5 +172,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

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

@ -0,0 +1,607 @@
package application
import (
"context"
"fmt"
"strconv"
"time"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/common/hex"
"git.grassecon.net/grassrootseconomics/common/phone"
"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"
"gopkg.in/leonelquinteros/gotext.v1"
)
// GetMpesaMaxLimit returns the max FROM token
// check if max/tokenDecimals > 0.1 for UX purposes and to prevent swapping of dust values
func (h *MenuHandlers) GetMpesaMaxLimit(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")
flag_incorrect_pool, _ := h.flagManager.GetFlag("flag_incorrect_pool")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
inputStr := string(input)
if inputStr == "0" || inputStr == "9" {
return res, nil
}
userStore := h.userdataStore
// Fetch session data
_, activeBal, _, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId)
if err != nil {
return res, err
}
// call the mpesa rates API to get the 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
}
txType := "swap"
mpesaAddress := config.DefaultMpesaAddress()
// Normalize the mpesa address to fetch the phone number
publicKeyNormalized, err := hex.NormalizeHex(mpesaAddress)
if err != nil {
logg.ErrorCtxf(ctx, "Failed to normalize alias address", "address", mpesaAddress, "error", err)
return res, err
}
// get the recipient's phone number from the address
recipientPhoneNumber, err := userStore.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", mpesaAddress)
recipientPhoneNumber = nil
}
// store it for future reference (TODO)
if err := userStore.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT_PHONE_NUMBER, recipientPhoneNumber); err != nil {
logg.ErrorCtxf(ctx, "Failed to write recipient phone number", "value", string(recipientPhoneNumber), "error", err)
return res, err
}
// fetch data for verification
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) {
txType = "normal"
// Save the transaction type
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 res, err
}
activeFloat, _ := strconv.ParseFloat(string(activeBal), 64)
ksh := fmt.Sprintf("%f", activeFloat*rates.Buy)
kshFormatted, _ := store.TruncateDecimalString(ksh, 0)
res.Content = l.Get(
"Enter the amount of Mpesa to get: (Max %s Ksh)\n",
kshFormatted,
)
return res, nil
}
// Resolve active pool address
activePoolAddress, err := h.resolveActivePoolAddress(ctx, sessionId)
if err != nil {
return res, err
}
// Check if sender token is swappable
canSwap, err := h.accountService.CheckTokenInPool(ctx, string(activePoolAddress), string(activeAddress))
if err != nil {
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)
res.FlagSet = append(res.FlagSet, flag_incorrect_pool)
return res, nil
}
// retrieve the max credit send amounts
_, maxRAT, err := h.calculateSendCreditLimits(ctx, activePoolAddress, activeAddress, recipientActiveAddress, publicKey, activeDecimal, recipientActiveDecimal)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on calculateSendCreditLimits", "error", err)
return res, nil
}
// Fallback if below minimum
maxFloat, _ := strconv.ParseFloat(maxRAT, 64)
if maxFloat < 0.1 {
formatted, _ := store.TruncateDecimalString(maxRAT, 2)
res.Content = formatted
res.FlagSet = append(res.FlagSet, flag_low_swap_amount)
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 the transaction type
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 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
}
maxKsh := maxFloat * rates.Buy
kshStr := fmt.Sprintf("%f", maxKsh)
kshFormatted, _ := store.TruncateDecimalString(kshStr, 0)
res.Content = l.Get(
"Enter the amount of Mpesa to get: (Max %s Ksh)\n",
kshFormatted,
)
return res, nil
}
// GetMpesaPreview displays the get mpesa preview and estimates
func (h *MenuHandlers) GetMpesaPreview(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 Ksh
inputStr := string(input)
if inputStr == "9" {
return res, nil
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
// call the mpesa rates API to get the 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
}
// Input in Ksh
kshAmount, err := strconv.ParseFloat(inputStr, 64)
if err != nil {
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)
if err != nil {
return res, err
}
transactionType, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE)
if err != nil {
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
}
if inputAmount > balanceValue {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
// Format the input amount to 2 decimal places
inputAmountStr := fmt.Sprintf("%f", inputAmount)
qouteInputAmount, _ := store.TruncateDecimalString(inputAmountStr, 2)
// store the inputAmountStr as the final amount (that will be sent)
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(inputAmountStr))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write send amount value entry with", "key", storedb.DATA_AMOUNT, "value", inputAmountStr, "error", err)
return res, err
}
res.Content = l.Get(
"You are sending %s %s in order to receive ~ %s ksh",
qouteInputAmount, swapData.ActiveSwapFromSym, inputStr,
)
return res, nil
}
// 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
}
if inputAmount > maxRATValue {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
formattedAmount := fmt.Sprintf("%f", inputAmount)
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 {
res.FlagSet = append(res.FlagSet, flag_api_call_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
sendOutputAmount := r.OutputAmount // amount of RAT
// 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
}
// 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
}
// covert for display
quoteInputStr := store.ScaleDownBalance(sendInputAmount, swapData.ActiveSwapFromDecimal)
// 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,
)
return res, nil
}
// InitiateGetMpesa calls the poolSwap, followed by the transfer and returns a confirmation based on the result.
func (h *MenuHandlers) InitiateGetMpesa(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_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
mpesaAddress := config.DefaultMpesaAddress()
swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId)
if err != nil {
return res, err
}
transactionType, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE)
if err != nil {
return res, err
}
if string(transactionType) == "normal" {
// Call TokenTransfer for the normal transaction
data, err := store.ReadTransactionData(ctx, h.userdataStore, sessionId)
if err != nil {
return res, err
}
finalAmountStr, err := store.ParseAndScaleAmount(data.Amount, data.ActiveDecimal)
if err != nil {
return res, err
}
tokenTransfer, err := h.accountService.TokenTransfer(ctx, finalAmountStr, data.PublicKey, mpesaAddress, data.ActiveAddress)
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 TokenTransfer", "error", err)
return res, nil
}
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.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
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_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
}
logg.InfoCtxf(ctx, "mpesa poolSwap before transfer", "swapTrackingId", poolSwap.TrackingId)
// 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)
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 TokenTransfer after swap", "error", err)
return res, nil
}
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.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
// SendMpesaMinLimit returns the min amount from the config
func (h *MenuHandlers) SendMpesaMinLimit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
inputStr := string(input)
if inputStr == "0" || inputStr == "9" {
return res, nil
}
// Fetch min amount from config/env
min := config.MinMpesaSendAmount()
// Convert to string
ksh := fmt.Sprintf("%f", min)
// Format (e.g., 100.0 -> 100)
kshFormatted, _ := store.TruncateDecimalString(ksh, 0)
res.Content = l.Get(
"Enter the amount of Mpesa to send: (Minimum %s Ksh)\n",
kshFormatted,
)
return res, nil
}
// SendMpesaPreview displays the send mpesa preview and estimates
func (h *MenuHandlers) SendMpesaPreview(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 Ksh
inputStr := string(input)
if inputStr == "0" || inputStr == "9" {
return res, nil
}
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
// call the mpesa rates API to get the 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
}
// Input in Ksh
kshAmount, err := strconv.ParseFloat(inputStr, 64)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
min := config.MinMpesaSendAmount()
max := config.MaxMpesaSendAmount()
if kshAmount > max || kshAmount < min {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = inputStr
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_invalid_amount)
// store the user's raw input amount
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(inputStr))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write amount inputStr entry with", "key", storedb.DATA_AMOUNT, "value", inputStr, "error", err)
return res, err
}
estimateValue := kshAmount / rates.Sell
estimateStr := fmt.Sprintf("%f", estimateValue)
estimateFormatted, _ := store.TruncateDecimalString(estimateStr, 2)
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,
)
return res, nil
}
// InitiateSendMpesa calls the trigger-onram API to initiate the purchase
func (h *MenuHandlers) InitiateSendMpesa(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_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
userStore := h.userdataStore
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
}
phoneNumber, err := phone.FormatToLocalPhoneNumber(sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on FormatToLocalPhoneNumber", "session-id", sessionId, "error", err)
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
return res, nil
}
defaultAsset := config.DefaultMpesaAsset()
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
}
amountInt, err := strconv.Atoi(string(amount))
if err != nil {
logg.ErrorCtxf(ctx, "failed to convert amount to int", "amount", string(amount), "error", err)
return res, err
}
// Call the trigger onramp API
triggerOnramp, err := h.accountService.MpesaTriggerOnramp(ctx, string(publicKey), phoneNumber, defaultAsset, amountInt)
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 MpesaTriggerOnramp", "error", err)
return res, nil
}
logg.InfoCtxf(ctx, "MpesaTriggerOnramp", "transactionCode", triggerOnramp.TransactionCode)
res.Content = l.Get("Your request has been sent. Thank you for using Sarafu")
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}

View File

@ -22,12 +22,12 @@ func (h *MenuHandlers) GetPools(ctx context.Context, sym string, input []byte) (
} }
userStore := h.userdataStore userStore := h.userdataStore
flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
// call the api to get a list of top 5 pools sorted by swaps // call the api to get a list of top 5 pools sorted by swaps
topPools, err := h.accountService.FetchTopPools(ctx) topPools, err := h.accountService.FetchTopPools(ctx)
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err)
return res, err return res, err
} }
@ -129,12 +129,12 @@ func (h *MenuHandlers) ViewPool(ctx context.Context, sym string, input []byte) (
} }
if poolData == nil { if poolData == nil {
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
// no match found. Call the API using the inputStr as the symbol // no match found. Call the API using the inputStr as the symbol
poolResp, err := h.accountService.RetrievePoolDetails(ctx, inputStr) poolResp, err := h.accountService.RetrievePoolDetails(ctx, inputStr)
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
return res, nil return res, nil
} }

View File

@ -41,7 +41,7 @@ func (h *MenuHandlers) LoadSwapToList(ctx context.Context, sym string, input []b
l.AddDomain("default") l.AddDomain("default")
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher") flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
inputStr := string(input) inputStr := string(input)
if inputStr == "0" { if inputStr == "0" {
@ -88,7 +88,7 @@ func (h *MenuHandlers) LoadSwapToList(ctx context.Context, sym string, input []b
// call the api using the ActivePoolAddress and ActiveVoucherAddress to check if it is part of the pool // call the api using the ActivePoolAddress and ActiveVoucherAddress to check if it is part of the pool
r, err := h.accountService.CheckTokenInPool(ctx, string(activePoolAddress), string(activeAddress)) r, err := h.accountService.CheckTokenInPool(ctx, string(activePoolAddress), string(activeAddress))
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on CheckTokenInPool", "error", err) logg.ErrorCtxf(ctx, "failed on CheckTokenInPool", "error", err)
return res, err return res, err
} }
@ -110,7 +110,7 @@ func (h *MenuHandlers) LoadSwapToList(ctx context.Context, sym string, input []b
// call the api using the activePoolAddress to get a list of SwapToSymbolsData // call the api using the activePoolAddress to get a list of SwapToSymbolsData
swapToList, err := h.accountService.GetPoolSwappableVouchers(ctx, string(activePoolAddress)) swapToList, err := h.accountService.GetPoolSwappableVouchers(ctx, string(activePoolAddress))
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err)
return res, err return res, err
} }
@ -165,7 +165,7 @@ func (h *MenuHandlers) SwapMaxLimit(ctx context.Context, sym string, input []byt
} }
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher") flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
flag_low_swap_amount, _ := h.flagManager.GetFlag("flag_low_swap_amount") flag_low_swap_amount, _ := h.flagManager.GetFlag("flag_low_swap_amount")
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher, flag_low_swap_amount) res.FlagReset = append(res.FlagReset, flag_incorrect_voucher, flag_low_swap_amount)
@ -202,9 +202,9 @@ func (h *MenuHandlers) SwapMaxLimit(ctx context.Context, sym string, input []byt
logg.InfoCtxf(ctx, "Call GetSwapFromTokenMaxLimit with:", "ActivePoolAddress", swapData.ActivePoolAddress, "ActiveSwapFromAddress", swapData.ActiveSwapFromAddress, "ActiveSwapToAddress", swapData.ActiveSwapToAddress, "publicKey", swapData.PublicKey) logg.InfoCtxf(ctx, "Call GetSwapFromTokenMaxLimit with:", "ActivePoolAddress", swapData.ActivePoolAddress, "ActiveSwapFromAddress", swapData.ActiveSwapFromAddress, "ActiveSwapToAddress", swapData.ActiveSwapToAddress, "publicKey", swapData.PublicKey)
r, err := h.accountService.GetSwapFromTokenMaxLimit(ctx, swapData.ActivePoolAddress, swapData.ActiveSwapFromAddress, swapData.ActiveSwapToAddress, swapData.PublicKey) r, err := h.accountService.GetSwapFromTokenMaxLimit(ctx, swapData.ActivePoolAddress, swapData.ActiveSwapFromAddress, swapData.ActiveSwapToAddress, swapData.PublicKey)
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on GetSwapFromTokenMaxLimit", "error", err) logg.ErrorCtxf(ctx, "failed on GetSwapFromTokenMaxLimit", "error", err)
return res, err return res, nil
} }
// Scale down the amount // Scale down the amount
@ -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
@ -310,8 +310,8 @@ func (h *MenuHandlers) SwapPreview(ctx context.Context, sym string, input []byte
// call the API to get the quote // call the API to get the quote
r, err := h.accountService.GetPoolSwapQuote(ctx, finalAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress) r, err := h.accountService.GetPoolSwapQuote(ctx, finalAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress)
if err != nil { if err != nil {
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.") res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err) logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil return res, nil
@ -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:",
@ -369,8 +364,8 @@ func (h *MenuHandlers) InitiateSwap(ctx context.Context, sym string, input []byt
// Call the poolSwap API // Call the poolSwap API
r, err := h.accountService.PoolSwap(ctx, swapAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress) r, err := h.accountService.PoolSwap(ctx, swapAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress)
if err != nil { if err != nil {
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.") res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err) logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil return res, nil

View File

@ -6,41 +6,39 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
"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)
@ -59,85 +57,192 @@ func (h *MenuHandlers) ValidateRecipient(ctx context.Context, sym string, input
switch recipientType { switch recipientType {
case "phone number": case "phone number":
// format the phone number return h.handlePhoneNumber(ctx, sessionId, recipient, &res)
formattedNumber, err := phone.FormatPhoneNumber(recipient) case "address":
if err != nil { return h.handleAddress(ctx, sessionId, recipient, &res)
logg.ErrorCtxf(ctx, "Failed to format the phone number: %s", recipient, "error", err) case "alias":
return res, err 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
} }
// Check if the phone number is registered
publicKey, err := store.ReadEntry(ctx, formattedNumber, storedb.DATA_PUBLIC_KEY) publicKey, err := store.ReadEntry(ctx, formattedNumber, storedb.DATA_PUBLIC_KEY)
if err != nil { if err != nil {
if db.IsNotFound(err) { if db.IsNotFound(err) {
logg.InfoCtxf(ctx, "Unregistered phone number: %s", recipient) logg.InfoCtxf(ctx, "Unregistered phone number", "recipient", recipient)
res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite) 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
} }
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", storedb.DATA_PUBLIC_KEY, "error", err) if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, publicKey); err != nil {
return res, err logg.ErrorCtxf(ctx, "Failed to write recipient", "value", string(publicKey), "error", err)
return *res, err
} }
// Save the publicKey as the recipient // Delegate to shared logic
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, publicKey) if err := h.determineAndSaveTransactionType(ctx, sessionId, publicKey, []byte(formattedNumber)); err != nil {
if err != nil { return *res, err
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", string(publicKey), "error", err)
return res, err
} }
case "address": return *res, nil
// checksum the address }
func (h *MenuHandlers) handleAddress(ctx context.Context, sessionId, recipient string, res *resource.Result) (resource.Result, error) {
store := h.userdataStore
address := ethutils.ChecksumAddress(recipient) address := ethutils.ChecksumAddress(recipient)
// Save the valid Ethereum address as the recipient if err := store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(address)); err != nil {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(address)) logg.ErrorCtxf(ctx, "Failed to write recipient address", "error", err)
if err != nil { return *res, err
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", recipient, "error", err)
return res, err
} }
case "alias": // 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_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
var aliasAddressResult string
if strings.Contains(recipient, ".") { if strings.Contains(recipient, ".") {
AliasAddress, err = h.accountService.CheckAliasAddress(ctx, recipient) alias, err := h.accountService.CheckAliasAddress(ctx, recipient)
if err == nil { if err == nil {
AliasAddressResult = AliasAddress.Address aliasAddressResult = alias.Address
} else { } else {
logg.ErrorCtxf(ctx, "failed to resolve alias", "alias", recipient, "error_alias_check", err) logg.ErrorCtxf(ctx, "Failed to resolve alias", "alias", recipient, "error", err)
} }
} else { } else {
//Perform a search for each search domain,break on first match
for _, domain := range config.SearchDomains() { for _, domain := range config.SearchDomains() {
fqdn := fmt.Sprintf("%s.%s", recipient, domain) fqdn := fmt.Sprintf("%s.%s", recipient, domain)
logg.InfoCtxf(ctx, "Resolving with fqdn alias", "alias", fqdn) logg.InfoCtxf(ctx, "Trying alias", "fqdn", fqdn)
AliasAddress, err = h.accountService.CheckAliasAddress(ctx, fqdn)
alias, err := h.accountService.CheckAliasAddress(ctx, fqdn)
if err == nil { if err == nil {
res.FlagReset = append(res.FlagReset, flag_api_error) res.FlagReset = append(res.FlagReset, flag_api_call_error)
AliasAddressResult = AliasAddress.Address aliasAddressResult = alias.Address
continue break
} else { } else {
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed to resolve alias", "alias", recipient, "error_alias_check", err) logg.ErrorCtxf(ctx, "Alias resolution failed", "alias", fqdn, "error", err)
return res, nil 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 +269,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,38 +295,224 @@ 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_call_error, _ := h.flagManager.GetFlag("flag_api_call_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 send_max_amount, return balance
if string(transactionType) == "normal" || sym == "send_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 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_call_error)
logg.ErrorCtxf(ctx, "failed on CheckTokenInPool", "error", err)
return res, nil
}
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_call_error)
logg.ErrorCtxf(ctx, "failed on calculateSendCreditLimits", "error", err)
return res, nil
}
// Fallback if below minimum
maxFloat, _ := strconv.ParseFloat(maxSAT, 64)
if maxFloat < 0.1 {
res.FlagReset = append(res.FlagReset, flag_swap_transaction)
res.Content = l.Get("Maximum amount: %s %s\nEnter amount:", formattedBalance, string(activeSym))
return res, nil
}
// Save max RAT amount to be used in validating the user's input
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, []byte(maxRAT))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write swap max amount (maxRAT)", "value", maxRAT, "error", err)
return res, err
}
// save swap related data for the swap preview
metadata := &dataserviceapi.TokenHoldings{
TokenAddress: string(recipientActiveAddress),
TokenSymbol: string(recipientActiveSym),
TokenDecimals: string(recipientActiveDecimal),
}
// Store the active swap_to data
if err := store.UpdateSwapToVoucherData(ctx, userStore, sessionId, metadata); err != nil {
logg.ErrorCtxf(ctx, "failed on UpdateSwapToVoucherData", "error", err)
return res, err
}
res.Content = l.Get(
"Credit Available: %s %s\n(You can swap up to %s %s -> %s %s).\nEnter %s amount:",
maxRAT,
string(recipientActiveSym),
maxSAT,
string(activeSym),
maxRAT,
string(recipientActiveSym),
string(recipientActiveSym),
)
return res, nil
}
func (h *MenuHandlers) getSessionData(ctx context.Context, sessionId string) (transactionType, activeBal, activeSym, activeAddress, publicKey, activeDecimal []byte, err error) {
store := h.userdataStore
transactionType, err = store.ReadEntry(ctx, sessionId, storedb.DATA_SEND_TRANSACTION_TYPE)
if err != nil {
return
}
activeBal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL)
if err != nil {
return
}
activeAddress, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
if err != nil {
return
}
activeSym, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM)
if err != nil {
return
}
publicKey, err = store.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
return
}
activeDecimal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_DECIMAL)
if err != nil {
return
}
return
}
func (h *MenuHandlers) getRecipientData(ctx context.Context, sessionId string) (recipientActiveSym, recipientActiveAddress, recipientActiveDecimal []byte, err error) {
store := h.userdataStore
recipientActiveSym, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM)
if err != nil {
return
}
recipientActiveAddress, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS)
if err != nil {
return
}
recipientActiveDecimal, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_DECIMAL)
if err != nil {
return
}
return
}
func (h *MenuHandlers) resolveActivePoolAddress(ctx context.Context, sessionId string) ([]byte, error) {
store := h.userdataStore
addr, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS)
if err == nil {
return addr, nil
}
if db.IsNotFound(err) {
defaultAddr := []byte(config.DefaultPoolAddress())
if err := store.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS, defaultAddr); err != nil {
logg.ErrorCtxf(ctx, "failed to write default pool address", "error", err)
return nil, err
}
return defaultAddr, nil
}
logg.ErrorCtxf(ctx, "failed to read active pool address", "error", err)
return nil, err
}
func (h *MenuHandlers) calculateSendCreditLimits(ctx context.Context, poolAddress, fromAddress, toAddress, publicKey, fromDecimal, toDecimal []byte) (string, string, error) {
creditSendMaxLimits, err := h.accountService.GetCreditSendMaxLimit(
ctx,
string(poolAddress),
string(fromAddress),
string(toAddress),
string(publicKey),
)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetCreditSendMaxLimit", "error", err)
return "", "", err
}
scaledSAT := store.ScaleDownBalance(creditSendMaxLimits.MaxSAT, string(fromDecimal))
formattedSAT, _ := store.TruncateDecimalString(string(scaledSAT), 2)
scaledRAT := store.ScaleDownBalance(creditSendMaxLimits.MaxRAT, string(toDecimal))
formattedRAT, _ := store.TruncateDecimalString(string(scaledRAT), 2)
return formattedSAT, formattedRAT, nil
} }
// ValidateAmount ensures that the given input is a valid amount and that // ValidateAmount ensures that the given input is a valid amount and that
@ -285,7 +586,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 +608,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
@ -371,8 +672,8 @@ func (h *MenuHandlers) InitiateTransaction(ctx context.Context, sym string, inpu
res.Content = l.Get("An unexpected error occurred. Please try again later.") res.Content = l.Get("An unexpected error occurred. Please try again later.")
} }
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on TokenTransfer", "error", err) logg.ErrorCtxf(ctx, "failed on TokenTransfer", "error", err)
return res, nil return res, nil
} }
@ -391,3 +692,217 @@ 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_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 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_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
}
swapTrackingId := poolSwap.TrackingId
logg.InfoCtxf(ctx, "send poolSwap before transfer", "swapTrackingId", swapTrackingId)
// TODO: remove this temporary time delay and replace with a swap and send endpoint
time.Sleep(1 * time.Second)
// 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_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 TokenTransfer", "error", err)
return res, nil
}
trackingId := tokenTransfer.TrackingId
logg.InfoCtxf(ctx, "send TokenTransfer after swap", "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

@ -277,7 +277,7 @@ func TestMaxAmount(t *testing.T) {
} }
} }
res, err := h.MaxAmount(ctx, "max_amount", []byte("")) res, err := h.MaxAmount(ctx, "send_max_amount", []byte(""))
if tt.expectedError { if tt.expectedError {
assert.Error(t, err) assert.Error(t, err)

View File

@ -20,7 +20,7 @@ func (h *MenuHandlers) CheckTransactions(ctx context.Context, sym string, input
} }
flag_no_transfers, _ := h.flagManager.GetFlag("flag_no_transfers") flag_no_transfers, _ := h.flagManager.GetFlag("flag_no_transfers")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
userStore := h.userdataStore userStore := h.userdataStore
logdb := h.logDb logdb := h.logDb
@ -33,11 +33,11 @@ func (h *MenuHandlers) CheckTransactions(ctx context.Context, sym string, input
// Fetch transactions from the API using the public key // Fetch transactions from the API using the public key
transactionsResp, err := h.accountService.FetchTransactions(ctx, string(publicKey)) transactionsResp, err := h.accountService.FetchTransactions(ctx, string(publicKey))
if err != nil { if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_call_error)
logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err)
return res, err return res, err
} }
res.FlagReset = append(res.FlagReset, flag_api_error) res.FlagReset = append(res.FlagReset, flag_api_call_error)
// Return if there are no transactions // Return if there are no transactions
if len(transactionsResp) == 0 { if len(transactionsResp) == 0 {

View File

@ -45,6 +45,26 @@ func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []b
res.FlagReset = append(res.FlagReset, flag_api_error) res.FlagReset = append(res.FlagReset, flag_api_error)
if len(vouchersResp) == 0 { if len(vouchersResp) == 0 {
// clear the current active voucher data if it exists
_, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM)
if err == nil {
firstVoucherMap := map[storedb.DataTyp]string{
storedb.DATA_ACTIVE_SYM: "",
storedb.DATA_ACTIVE_BAL: "",
storedb.DATA_ACTIVE_DECIMAL: "",
storedb.DATA_ACTIVE_ADDRESS: "",
}
for key, value := range firstVoucherMap {
if err := userStore.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil {
logg.ErrorCtxf(ctx, "Failed to reset active voucher data", "key", key, "error", err)
return res, err
}
}
logg.InfoCtxf(ctx, "Default voucher reset")
}
res.FlagSet = append(res.FlagSet, flag_no_active_voucher) res.FlagSet = append(res.FlagSet, flag_no_active_voucher)
return res, nil return res, nil
} }

View File

@ -85,7 +85,8 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService)
ls.DbRs.AddLocalFunc("validate_recipient", appHandlers.ValidateRecipient) ls.DbRs.AddLocalFunc("validate_recipient", appHandlers.ValidateRecipient)
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("send_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,16 @@ 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.DbRs.AddLocalFunc("get_mpesa_max_limit", appHandlers.GetMpesaMaxLimit)
ls.DbRs.AddLocalFunc("get_mpesa_preview", appHandlers.GetMpesaPreview)
ls.DbRs.AddLocalFunc("initiate_get_mpesa", appHandlers.InitiateGetMpesa)
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.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}} {{.send_max_amount}}
Enter amount:

View File

@ -1,7 +1,8 @@
LOAD reset_transaction_amount 0 LOAD reset_transaction_amount 10
LOAD max_amount 40 RELOAD reset_transaction_amount
RELOAD max_amount LOAD send_max_amount 0
MAP max_amount RELOAD send_max_amount
MAP send_max_amount
MOUT back 0 MOUT back 0
HALT HALT
LOAD validate_amount 64 LOAD validate_amount 64
@ -9,7 +10,7 @@ RELOAD validate_amount
CATCH api_failure flag_api_call_error 1 CATCH api_failure flag_api_call_error 1
CATCH invalid_amount flag_invalid_amount 1 CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0 INCMP _ 0
LOAD get_recipient 0 LOAD get_recipient 100
LOAD get_sender 64 LOAD get_sender 64
LOAD get_amount 32 LOAD get_amount 32
INCMP transaction_pin * INCMP transaction_pin *

View File

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

View File

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

View File

@ -0,0 +1,19 @@
LOAD reset_transaction_amount 10
LOAD credit_max_amount 160
RELOAD credit_max_amount
CATCH api_failure flag_api_call_error 1
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 @@
{{.get_mpesa_max_limit}}

View File

@ -0,0 +1,10 @@
CATCH no_voucher flag_no_active_voucher 1
LOAD get_mpesa_max_limit 0
RELOAD get_mpesa_max_limit
MAP get_mpesa_max_limit
MOUT back 0
MOUT quit 9
HALT
INCMP _ 0
INCMP quit 9
INCMP get_mpesa_confirmation *

View File

@ -0,0 +1,3 @@
{{.get_mpesa_preview}}
Please enter your PIN to confirm. You will get an SMS shortly:

View File

@ -0,0 +1,13 @@
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
LOAD authorize_account 6
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
INCMP _ 0
INCMP quit 9
INCMP initiate_get_mpesa *

View File

@ -0,0 +1,3 @@
{{.get_mpesa_preview}}
Tafadhali weka PIN yako kudhibitisha. Utapokea ujumbe wa SMS:

View File

@ -0,0 +1 @@
Get M-Pesa

View File

@ -0,0 +1 @@
Pokea M-Pesa

View File

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

View File

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

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

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

View File

@ -0,0 +1,6 @@
MAP send_mpesa_preview
MOUT retry 1
MOUT quit 9
HALT
INCMP ^ 1
INCMP quit 9

View File

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

View File

@ -45,3 +45,30 @@ 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"
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"
msgid "Your request has been sent. You will receive ~ %s ksh"
msgstr "Ombi lako limetumwa. Utapokea ~ %s ksh"
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 "Your request has been sent. Thank you for using Sarafu"
msgstr "Ombi lako limetumwa. Asante kwa kutumia huduma ya Sarafu"

View File

@ -7,18 +7,22 @@ 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 mpesa 6
MOUT account 7
MOUT help 8
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 mpesa 6
INCMP my_account 7
INCMP help 8
INCMP quit 9 INCMP quit 9
INCMP . * INCMP . *

View File

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

View File

@ -0,0 +1,9 @@
MAP check_balance
MOUT get_mpesa 1
MOUT send_mpesa 2
MOUT quit 9
HALT
INCMP get_mpesa 1
INCMP send_mpesa 2
INCMP quit 9
INCMP . *

View File

@ -0,0 +1 @@
M-Pesa

View File

@ -31,7 +31,8 @@ flag,flag_back_set,37,this is set when it is a back navigation
flag,flag_account_blocked,38,this is set when an account has been blocked after the allowed incorrect PIN attempts have been exceeded flag,flag_account_blocked,38,this is set when an account has been blocked after the allowed incorrect PIN attempts have been exceeded
flag,flag_invalid_pin,39,this is set when the given PIN is invalid(is less than or more than 4 digits) flag,flag_invalid_pin,39,this is set when the given PIN is invalid(is less than or more than 4 digits)
flag,flag_alias_set,40,this is set when an account alias has been assigned to a user flag,flag_alias_set,40,this is set when an account alias has been assigned to a user
flag,flag_account_pin_reset,41,this is set on an account when an admin triggers a PIN reset for themflag,flag_incorrect_pool,39,this is set when the user selects an invalid pool flag,flag_account_pin_reset,41,this is set on an account when an admin triggers a PIN reset for them
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

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

View File

@ -0,0 +1,9 @@
LOAD send_mpesa_min_limit 0
RELOAD send_mpesa_min_limit
MAP send_mpesa_min_limit
MOUT back 0
MOUT quit 9
HALT
INCMP _ 0
INCMP quit 9
INCMP send_mpesa_confirmation *

View File

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

View File

@ -0,0 +1,12 @@
LOAD send_mpesa_preview 0
MAP send_mpesa_preview
CATCH invalid_send_mpesa_amount flag_invalid_amount 1
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_send_mpesa *

View File

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

View File

@ -0,0 +1 @@
Send M-Pesa

View File

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

View File

@ -0,0 +1 @@
Badilisha

View File

@ -1,2 +1,2 @@
Chagua nambari au ishara ya sarafu kubadilisha KWENDA: Chagua nambari au ishara ya sarafu unayotaka kupokea.
{{.swap_to_list}} {{.swap_to_list}}

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

@ -16,9 +16,9 @@ func TestReadSwapData(t *testing.T) {
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",
} }
@ -53,12 +53,13 @@ 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_TEMPORARY_VALUE: "temp",
storedb.DATA_PUBLIC_KEY: publicKey, storedb.DATA_PUBLIC_KEY: publicKey,
storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT: "1339482", storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT: "1339482",
storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: "6", storedb.DATA_ACTIVE_DECIMAL: "6",
storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e", storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e",
storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", storedb.DATA_ACTIVE_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe",
storedb.DATA_ACTIVE_SWAP_FROM_SYM: "AMANI", storedb.DATA_ACTIVE_SYM: "AMANI",
storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a", storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD", storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD",
storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: "18", storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: "18",
@ -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",