Compare commits

...

161 Commits

Author SHA1 Message Date
alfred-mk
e6a369dcdd remove unused code 2024-10-31 14:44:42 +03:00
alfred-mk
b8bbd88078 retain the temporary data for it to be overwritten 2024-10-31 14:26:28 +03:00
alfred-mk
a31cac4e50 Merge branch 'master' into voucher-data 2024-10-30 18:46:21 +03:00
alfred-mk
8b399781e8 make the VoucherMetadata more descriptive 2024-10-30 18:40:03 +03:00
alfred-mk
caafe495be use the dataserviceapi structs 2024-10-30 18:30:55 +03:00
alfred-mk
66b34eaea4 get the ussd-data-service package 2024-10-30 18:21:49 +03:00
alfred-mk
adaa0c63ef Extract common db operations for the test 2024-10-30 14:12:42 +03:00
carlos
843b0d1e7e Merge pull request 'Add reverse sessionid address lookup' (#146) from lash/reverse-session into master
Reviewed-on: #146
2024-10-30 10:35:55 +01:00
lash
cb997159f7 Add reverse sessionid address lookup 2024-10-30 00:59:59 +00:00
alfred-mk
7241cdbfcb Updated the tests 2024-10-30 00:53:13 +03:00
alfred-mk
0480c02633 Moved voucher related functions to the utils package 2024-10-30 00:52:23 +03:00
alfred-mk
03c32fd265 Merge branch 'master' into voucher-data 2024-10-28 18:40:37 +03:00
727f54ee57 Merge pull request 'tests-refactor' (#137) from tests-refactor into master
Reviewed-on: #137
2024-10-28 16:28:41 +01:00
alfred-mk
2c361e5b96 Added tests related to the vouchers list 2024-10-28 16:30:13 +03:00
alfred-mk
131f3bcf46 Added the prefixDb to the Handlers struct~ 2024-10-28 16:27:24 +03:00
alfred-mk
520f5abdcd Added the subPrefixDb mock 2024-10-28 16:23:44 +03:00
alfred-mk
5d294b663c Added the PrefixDb interface 2024-10-28 16:21:31 +03:00
alfred-mk
dc782d87a8 Updated the TestSetVoucher 2024-10-28 11:20:06 +03:00
alfred-mk
ddae746b9d formatted code 2024-10-28 11:07:59 +03:00
Carlosokumu
4e1b2d5ddb update account services 2024-10-25 18:01:52 +03:00
Carlosokumu
d8800a665d Merge branch 'master' into tests-refactor 2024-10-25 17:48:40 +03:00
alfred-mk
6c3ff0e9db Merge branch 'master' into voucher-data 2024-10-25 17:37:08 +03:00
alfred-mk
3ef64271e7 Store and retrieve the voucher decimals and contract address 2024-10-25 17:24:09 +03:00
2dee47404d Merge pull request 'menu-voucherlist' (#101) from menu-voucherlist into master
Reviewed-on: #101
2024-10-25 15:59:46 +02:00
Carlosokumu
3792bfdc1f run mod tidy 2024-10-25 09:38:09 +03:00
Carlosokumu
a0e97cfe5b remove test account service implementation 2024-10-25 09:37:16 +03:00
Carlosokumu
a3be98c514 update test account service 2024-10-25 09:36:43 +03:00
Carlosokumu
2b34b3a75c Merge branch 'master' into tests-refactor 2024-10-25 09:14:16 +03:00
alfred-mk
b4454f7517 Added context to FetchVouchers 2024-10-24 20:44:29 +03:00
alfred-mk
d9c660b8ea Merge branch 'master' into menu-voucherlist 2024-10-24 20:39:43 +03:00
alfred-mk
d113ea82fd Add dynamic send_amount and session_id 2024-10-24 20:21:28 +03:00
alfred-mk
a92c640cb7 Updated tests 2024-10-24 18:15:54 +03:00
alfred-mk
728815f0c6 Include the active symbol in the send menu 2024-10-24 17:50:37 +03:00
6b5d3f74d1 Merge pull request 'api-context' (#127) from api-context into master
Reviewed-on: #127
2024-10-24 16:45:37 +02:00
Carlosokumu
bee9ad5ff5 Merge remote-tracking branch 'refs/remotes/origin/api-context' into api-context 2024-10-24 17:39:11 +03:00
Carlosokumu
6e7b46666e ensure mod match with master 2024-10-24 17:34:42 +03:00
Carlosokumu
383f074cae update method signatures 2024-10-24 17:32:08 +03:00
Carlosokumu
d678a639b8 Merge branch 'master' into api-context 2024-10-24 17:07:11 +03:00
453fea569a Merge pull request 'api-structs' (#117) from api-structs into master
Reviewed-on: #117
2024-10-24 15:53:46 +02:00
carlos
5c75e35fe0 Delete coverage.html
delete cover.html
2024-10-24 15:45:52 +02:00
carlos
cff50538fa Delete cover.out
delete cover.out
2024-10-24 15:45:29 +02:00
carlos
69a4530269 Delete services/registration/locale/swa/default.mo
remove dev file
2024-10-24 15:41:47 +02:00
Carlosokumu
db19e38717 Merge remote-tracking branch 'refs/remotes/origin/api-structs' into api-structs 2024-10-24 16:37:07 +03:00
Carlosokumu
c796bbdcfc correct create endpoint 2024-10-24 16:36:09 +03:00
Carlosokumu
2b34a0900c check for status code and unmarshal on errResponse 2024-10-24 16:35:53 +03:00
alfred-mk
e15bac98a5 Merge branch 'master' into menu-voucherlist 2024-10-24 16:31:49 +03:00
alfred-mk
ee9a683eb0 Merge branch 'master' into menu-voucherlist 2024-10-24 16:30:44 +03:00
alfred-mk
9274e585bd Merge branch 'master' into menu-voucherlist 2024-10-24 16:26:32 +03:00
Carlosokumu
57a49819f4 Merge branch 'master' into api-structs 2024-10-24 16:22:41 +03:00
Carlosokumu
abc01b7cee change to local setup endpoint 2024-10-24 16:21:34 +03:00
d19c20a9d7 Merge branch 'master' into api-structs 2024-10-24 15:19:06 +02:00
0c6144d262 Merge pull request 'send-menu-update' (#131) from send-menu-update into master
Reviewed-on: #131
2024-10-24 15:15:11 +02:00
alfred-mk
ec4e44a27c Merge branch 'master' into send-menu-update 2024-10-24 15:58:59 +03:00
69eb57f794 Merge branch 'master' into api-context 2024-10-24 14:51:25 +02:00
2347d64acc Merge pull request 'check-balance-update' (#132) from check-balance-update into master
Reviewed-on: #132
2024-10-24 14:50:59 +02:00
alfred-mk
39c0560abe Updated the test 2024-10-24 15:21:48 +03:00
alfred-mk
75459f852b Return the full balance string 2024-10-24 15:12:57 +03:00
alfred-mk
6200728435 Updated the menuhander test 2024-10-24 14:45:15 +03:00
alfred-mk
579b46db65 Replace the public key with the sessionId 2024-10-24 14:34:12 +03:00
Carlosokumu
f99486c190 correct import 2024-10-24 12:07:00 +03:00
Carlosokumu
57a07af8ca correct import 2024-10-24 12:06:44 +03:00
Carlosokumu
5692440099 move to testutil 2024-10-24 12:05:27 +03:00
Carlosokumu
651969668f move to testutil 2024-10-24 12:05:11 +03:00
Carlosokumu
f3a028f1fc move to testutil 2024-10-24 12:03:32 +03:00
Carlosokumu
9f2d57ea03 update tests 2024-10-24 10:10:14 +03:00
Carlosokumu
d74a4cc33b update service mock method signatures 2024-10-24 10:10:03 +03:00
Carlosokumu
308ca99fb0 remove reload account creation 2024-10-24 10:02:32 +03:00
Carlosokumu
08e709f1b3 check for error responses 2024-10-24 10:02:15 +03:00
Carlosokumu
9bc9d04a49 use errresponse and okresponse for deserialization 2024-10-24 10:00:38 +03:00
alfred-mk
a553731f02 Cleaned up code 2024-10-23 17:45:41 +03:00
alfred-mk
4011597d9c Check specific db error 2024-10-23 14:02:13 +03:00
alfred-mk
176473aa26 Rename prefix to vouchers 2024-10-23 13:54:42 +03:00
Carlosokumu
b41e52af63 pass context.Context 2024-10-23 12:45:54 +03:00
Carlosokumu
fb32dde136 run go mod tidy 2024-10-23 12:45:10 +03:00
Carlosokumu
1e6cf6a33a run mod tidy 2024-10-23 11:08:51 +03:00
Carlosokumu
2725323f16 Merge branch 'master' into tests-refactor 2024-10-23 11:06:10 +03:00
alfred-mk
5f1ee396d8 Fix PIN being requested twice 2024-10-23 06:35:19 +03:00
Carlosokumu
5f2c6cce16 add required track endpoint 2024-10-22 21:36:28 +03:00
Carlosokumu
3179ec1f62 add local track endpoint 2024-10-22 21:35:52 +03:00
Carlosokumu
b9a63f3c6f merge dep 2024-10-22 21:30:48 +03:00
Carlosokumu
1d255372b2 Merge branch 'master' into api-structs 2024-10-22 21:05:51 +03:00
Carlosokumu
306666a15e load and reload after halt 2024-10-22 20:37:30 +03:00
Carlosokumu
e05dbb4885 check for back option 2024-10-22 20:36:58 +03:00
alfred-mk
637e107525 Merge branch 'master' into menu-voucherlist 2024-10-22 17:10:51 +03:00
59d0446020 Merge pull request 'postgres-switch' (#113) from postgres-switch into master
Reviewed-on: #113
2024-10-22 16:04:56 +02:00
alfred-mk
f37ec13c75 polish code 2024-10-22 16:31:29 +03:00
alfred-mk
0547bc7e5f added send menu test with dynamic max_amount 2024-10-22 14:24:22 +03:00
alfred-mk
02cb75f97a increase the size to resolve sink error 2024-10-21 17:14:59 +03:00
Carlosokumu
9c75942b0b chore: remove commented code 2024-10-21 16:54:16 +03:00
alfred-mk
5909659fa9 Use dynamic balance 2024-10-21 16:52:07 +03:00
Carlosokumu
eaac771722 move into testtag package 2024-10-21 16:48:24 +03:00
Carlosokumu
f3a5178de7 update imports 2024-10-21 16:47:43 +03:00
Carlosokumu
549d09890b update imports 2024-10-21 16:47:25 +03:00
Carlosokumu
4a9ef6b5f2 move test account service to testutil 2024-10-21 16:47:00 +03:00
Carlosokumu
961d8d1a5c update package import 2024-10-21 16:46:29 +03:00
Carlosokumu
a904cdbf44 move driver to testutil 2024-10-21 16:45:06 +03:00
Carlosokumu
3af943f77c move account service tags switch to test tags package 2024-10-21 16:44:02 +03:00
Carlosokumu
14455f65cb move account service to testutil 2024-10-21 16:43:24 +03:00
Carlosokumu
0c08654df3 move driver to testutil 2024-10-21 16:42:57 +03:00
Carlosokumu
acd0af25c6 catch api call error 2024-10-21 11:11:04 +03:00
Carlosokumu
66d2e2e838 add custodial api structs 2024-10-21 11:10:42 +03:00
Carlosokumu
4beeb9986a use importable api structs 2024-10-21 11:10:23 +03:00
Carlosokumu
d81bc0eefb use importable api structs 2024-10-21 11:10:01 +03:00
Carlosokumu
5b0a383513 use importable api structs 2024-10-21 11:09:37 +03:00
Carlosokumu
367d3f0593 remove manually added api.go file 2024-10-21 11:07:32 +03:00
alfred-mk
3bf2045f15 added FetchVouchers to the TestAccountService 2024-10-19 16:07:40 +03:00
alfred-mk
aced3e4fc3 Merge branch 'master' into menu-voucherlist 2024-10-19 15:38:54 +03:00
alfred-mk
25bc7006a4 show a balance of 0 and prevent sending when no voucher exists 2024-10-18 18:31:40 +03:00
Carlosokumu
f643aa4d14 update tests 2024-10-18 17:37:04 +03:00
Carlosokumu
b8860478b6 implement expected structs 2024-10-18 17:36:51 +03:00
Carlosokumu
847b91ca9e Merge branch 'menu-api-errors' into api-structs 2024-10-18 17:15:05 +03:00
Carlosokumu
353e24de33 define api structs 2024-10-18 17:04:43 +03:00
Carlosokumu
1c57c95d93 use api structs 2024-10-18 17:04:02 +03:00
Carlosokumu
128a354b34 use api structs 2024-10-18 17:03:48 +03:00
Carlosokumu
81c4189c8e update tests 2024-10-18 17:02:08 +03:00
Carlosokumu
b8e12e5215 update api endpoints 2024-10-18 17:01:21 +03:00
Carlosokumu
4f04362835 pass error description to error response 2024-10-17 16:08:18 +03:00
alfred-mk
eea3be3a39 resolve failing test 2024-10-17 13:49:56 +03:00
alfred-mk
849a950fbc Merge branch 'master' into menu-voucherlist 2024-10-17 13:44:31 +03:00
Carlosokumu
f73b7a8b04 setup api responses 2024-10-16 22:40:40 +03:00
alfred-mk
d05c666513 Merge branch 'master' into menu-voucherlist 2024-10-15 17:08:48 +03:00
alfred-mk
a336856c9b use separate functions to process voucher data 2024-10-15 17:02:51 +03:00
alfred-mk
f378f15422 updated tests 2024-10-15 16:29:51 +03:00
alfred-mk
b2fb9faf6c use the active balance to validate the amount 2024-10-15 16:17:33 +03:00
alfred-mk
859e203a00 removed debug comments 2024-10-15 15:54:43 +03:00
alfred-mk
6c6af5ec21 set a default voucher as the active sym if none exists 2024-10-15 15:46:02 +03:00
alfred-mk
8ed98b3e6c resolve case issue 2024-10-15 14:53:00 +03:00
alfred-mk
7df77a1343 updated the ValidateAmount to also check the active symbol, updated tests 2024-10-12 20:07:06 +03:00
alfred-mk
f5dbfe553d use the check_balance for the max_amount 2024-10-12 20:06:00 +03:00
alfred-mk
7fe8f0b7d5 updated the args under FetchVouchers 2024-10-12 18:03:13 +03:00
alfred-mk
a9f9867976 added the TestSetVoucher 2024-10-12 17:36:00 +03:00
alfred-mk
b6d24bf929 update the TestCheckBalance 2024-10-12 16:29:12 +03:00
alfred-mk
e9684fcf45 check whether the active symbol is empty 2024-10-12 16:28:02 +03:00
alfred-mk
a5b1c5b74e cleaned up the code 2024-10-12 14:32:40 +03:00
alfred-mk
672eebb8fb display the balance based on the symbol 2024-10-11 09:42:08 +03:00
alfred-mk
8f834b3d76 ensure that a user is authorized 2024-10-11 09:41:47 +03:00
alfred-mk
6c93fb76b1 correctly added the flag and the flag count 2024-10-11 09:36:43 +03:00
alfred-mk
ea2df55295 use go-vise v0.2.0 2024-10-10 15:18:13 +03:00
alfred-mk
7c08a0f0af add temp and active balance 2024-10-10 14:48:23 +03:00
alfred-mk
9ad7d5a522 set the active symbol 2024-10-09 15:39:06 +03:00
alfred-mk
6c904a8b3f add the temporary and active symbol 2024-10-09 15:38:19 +03:00
alfred-mk
9ccb6cc066 use 99 as the quit 2024-10-09 15:05:59 +03:00
Carlosokumu
2c98a8e133 read token list from json file 2024-10-08 14:34:21 +03:00
Carlosokumu
4b6fd35e7a define importable voucher response 2024-10-08 13:43:57 +03:00
Carlosokumu
b3c7a3a337 Merge remote-tracking branch 'refs/remotes/origin/menu-voucherlist' into menu-voucherlist 2024-10-08 13:43:19 +03:00
Carlosokumu
7b374ad801 define importable token list 2024-10-08 13:41:09 +03:00
alfred-mk
cb4a52e4f2 view a selected voucher and verify the PIN 2024-10-07 16:16:57 +03:00
alfred-mk
06ebcc0f07 added swahili template 2024-10-07 16:15:13 +03:00
alfred-mk
e4ed9a65bb store the symbols and balances 2024-10-07 13:49:12 +03:00
alfred-mk
755899be4e store the pre-rendered vouchers list 2024-10-07 08:59:15 +03:00
alfred-mk
4dede757d2 Merge branch 'lash/subprefix' into menu-voucherlist 2024-10-07 08:06:37 +03:00
alfred-mk
517f980664 get the vouchers list and store in gdbm 2024-10-05 16:56:49 +03:00
alfred-mk
31aea6b807 added model and func to fetch vouchers from the API 2024-09-28 14:07:37 +03:00
alfred-mk
3a46fda769 use save_temporary_pin and updated tests 2024-09-28 12:26:37 +03:00
alfred-mk
c7bdfe90b7 Merge branch 'master' into menu-voucherlist 2024-09-28 11:07:36 +03:00
alfred-mk
60134f14e9 Merge branch 'master' into menu-voucherlist 2024-09-27 19:11:35 +03:00
alfred-mk
4c33945081 use latest go-vise update 2024-09-27 19:10:08 +03:00
alfred-mk
221db4e998 return a numbered list of vouchers 2024-09-25 16:06:06 +03:00
alfred-mk
0e376e0d9e include back and quit 2024-09-25 16:03:08 +03:00
Carlosokumu
7aa44caea2 add voucher nodes 2024-09-25 15:57:23 +03:00
Carlosokumu
188cb573dd add dummy vouchers list 2024-09-25 13:27:13 +03:00
66 changed files with 1787 additions and 628 deletions

View File

@@ -12,6 +12,7 @@ DB_SSLMODE=disable
DB_TIMEZONE=Africa/Nairobi
#External API Calls
CREATE_ACCOUNT_URL=https://custodial.sarafu.africa/api/account/create
CREATE_ACCOUNT_URL=http://localhost:5003/api/v2/account/create
TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/
BALANCE_URL=https://custodial.sarafu.africa/api/account/status/
TRACK_URL=http://localhost:5003/api/v2/account/status

18
common/hex.go Normal file
View File

@@ -0,0 +1,18 @@
package common
import (
"encoding/hex"
)
func NormalizeHex(s string) (string, error) {
if len(s) >= 2 {
if s[:2] == "0x" {
s = s[2:]
}
}
r, err := hex.DecodeString(s)
if err != nil {
return "", err
}
return hex.EncodeToString(r), nil
}

View File

@@ -6,11 +6,13 @@ var (
CreateAccountURL string
TrackStatusURL string
BalanceURL string
TrackURL string
)
// LoadConfig initializes the configuration values after environment variables are loaded.
func LoadConfig() {
CreateAccountURL = initializers.GetEnv("CREATE_ACCOUNT_URL", "https://custodial.sarafu.africa/api/account/create")
CreateAccountURL = initializers.GetEnv("CREATE_ACCOUNT_URL", "http://localhost:5003/api/v2/account/create")
TrackStatusURL = initializers.GetEnv("TRACK_STATUS_URL", "https://custodial.sarafu.africa/api/track/")
BalanceURL = initializers.GetEnv("BALANCE_URL", "https://custodial.sarafu.africa/api/account/status/")
TrackURL = initializers.GetEnv("TRACK_URL", "http://localhost:5003/api/v2/account/status")
}

24
go.mod
View File

@@ -1,27 +1,43 @@
module git.grassecon.net/urdt/ussd
go 1.22.6
go 1.23.0
toolchain go1.23.2
require (
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b
github.com/alecthomas/assert/v2 v2.2.2
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
github.com/peteole/testdata-loader v0.3.0
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
)
require github.com/joho/godotenv v1.5.1 // indirect
require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/joho/godotenv v1.5.1
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
)
require (
github.com/alecthomas/participle/v2 v2.0.0 // indirect
github.com/alecthomas/repr v0.2.0 // indirect
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0
github.com/x448/float16 v0.8.4 // indirect

46
go.sum
View File

@@ -1,7 +1,3 @@
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y=
git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
git.defalsify.org/vise.git v0.2.0 h1:X2ZgiGRq4C+9qOlDMP0b/oE5QHjVQNT4aEFZB88ST0Q=
git.defalsify.org/vise.git v0.2.0/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M=
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU=
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
@@ -12,33 +8,67 @@ 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/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE=
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
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/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo=
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU=
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw=
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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
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.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s=
github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/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.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc=
gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -61,8 +61,8 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn
ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
ls.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin)
ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin)
ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin)
ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier)
ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus)
ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize)
@@ -89,11 +89,15 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn
ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob)
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob)
ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction)
ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin)
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
return ussdHandlers, nil
}

View File

@@ -1,29 +1,35 @@
package server
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"os"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
)
var (
okResponse api.OKResponse
errResponse api.ErrResponse
)
type AccountServiceInterface interface {
CheckBalance(publicKey string) (*models.BalanceResponse, error)
CreateAccount() (*models.AccountResponse, error)
CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error)
CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error)
CreateAccount(ctx context.Context) (*api.OKResponse, error)
CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error)
TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error)
FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error)
}
type AccountService struct {
}
type TestAccountService struct {
}
// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
//
// Parameters:
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
@@ -32,9 +38,9 @@ type TestAccountService struct {
// Returns:
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil.
func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
resp, err := http.Get(config.TrackStatusURL + trackingId)
// If no error occurs, this will be nil
func (as *AccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) {
resp, err := http.Get(config.BalanceURL + trackingId)
if err != nil {
return nil, err
}
@@ -44,18 +50,61 @@ func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackSt
if err != nil {
return nil, err
}
var trackResp models.TrackStatusResponse
err = json.Unmarshal(body, &trackResp)
if err != nil {
return nil, err
}
return &trackResp, nil
}
func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) {
var err error
// Construct the URL with the path parameter
url := fmt.Sprintf("%s/%s", config.TrackURL, publicKey)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GE-KEY", "xd")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
errResponse.Description = err.Error()
return nil, err
}
if resp.StatusCode >= http.StatusBadRequest {
err := json.Unmarshal([]byte(body), &errResponse)
if err != nil {
return nil, err
}
return nil, errors.New(errResponse.Description)
}
err = json.Unmarshal([]byte(body), &okResponse)
if err != nil {
return nil, err
}
if len(okResponse.Result) == 0 {
return nil, errors.New("Empty api result")
}
return &okResponse, nil
}
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
// Parameters:
// - publicKey: The public key associated with the account whose balance needs to be checked.
func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) {
resp, err := http.Get(config.BalanceURL + publicKey)
if err != nil {
return nil, err
@@ -79,75 +128,58 @@ func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceRespons
// If there is an error during the request or processing, this will be nil.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil.
func (as *AccountService) CreateAccount() (*models.AccountResponse, error) {
resp, err := http.Post(config.CreateAccountURL, "application/json", nil)
func (as *AccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
var err error
// Create a new request
req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GE-KEY", "xd")
resp, err := http.DefaultClient.Do(req)
if err != nil {
errResponse.Description = err.Error()
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var accountResp models.AccountResponse
err = json.Unmarshal(body, &accountResp)
if resp.StatusCode >= http.StatusBadRequest {
err := json.Unmarshal([]byte(body), &errResponse)
if err != nil {
return nil, err
}
return nil, errors.New(errResponse.Description)
}
err = json.Unmarshal([]byte(body), &okResponse)
if err != nil {
return nil, err
}
return &accountResp, nil
}
func (tas *TestAccountService) CreateAccount() (*models.AccountResponse, error) {
return &models.AccountResponse{
Ok: true,
Result: struct {
CustodialId json.Number `json:"custodialId"`
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
}{
CustodialId: json.Number("182"),
PublicKey: "0x48ADca309b5085852207FAaf2816eD72B52F527C",
TrackingId: "28ebe84d-b925-472c-87ae-bbdfa1fb97be",
},
}, nil
}
func (tas *TestAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
balanceResponse := &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
if len(okResponse.Result) == 0 {
return nil, errors.New("Empty api result")
}
return balanceResponse, nil
return &okResponse, nil
}
func (tas *TestAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
trackResponse := &models.TrackStatusResponse{
Ok: true,
Result: struct {
Transaction struct {
CreatedAt time.Time "json:\"createdAt\""
Status string "json:\"status\""
TransferValue json.Number "json:\"transferValue\""
TxHash string "json:\"txHash\""
TxType string "json:\"txType\""
}
}{
Transaction: models.Transaction{
CreatedAt: time.Now(),
Status: "SUCCESS",
TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def",
TxType: "transfer",
},
},
// FetchVouchers retrieves the token holdings for a given public key from the custodial holdings API endpoint
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
file, err := os.Open("sample_tokens.json")
if err != nil {
return nil, err
}
return trackResponse, nil
defer file.Close()
var holdings models.VoucherHoldingResponse
if err := json.NewDecoder(file).Decode(&holdings); err != nil {
return nil, err
}
return &holdings, nil
}

View File

@@ -10,6 +10,7 @@ import (
"strings"
"git.defalsify.org/vise.git/asm"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
"git.defalsify.org/vise.git/cache"
"git.defalsify.org/vise.git/db"
@@ -20,13 +21,19 @@ import (
"git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/utils"
"git.grassecon.net/urdt/ussd/common"
"gopkg.in/leonelquinteros/gotext.v1"
"git.grassecon.net/urdt/ussd/internal/storage"
)
var (
logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
scriptDir = path.Join("services", "registration")
translationDir = path.Join(scriptDir, "locale")
okResponse *api.OKResponse
errResponse *api.ErrResponse
backOption = []byte("0")
)
// FlagManager handles centralized flag management
@@ -59,6 +66,7 @@ type Handlers struct {
userdataStore utils.DataStore
flagManager *asm.FlagParser
accountService server.AccountServiceInterface
prefixDb storage.PrefixDb
}
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, accountService server.AccountServiceInterface) (*Handlers, error) {
@@ -68,10 +76,14 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, accountService s
userDb := &utils.UserDataStore{
Db: userdataStore,
}
// Instantiate the SubPrefixDb with "vouchers" prefix
prefixDb := storage.NewSubPrefixDb(userdataStore, []byte("vouchers"))
h := &Handlers{
userdataStore: userDb,
flagManager: appFlags,
accountService: accountService,
prefixDb: prefixDb,
}
return h, nil
}
@@ -136,23 +148,35 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r
}
func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
accountResp, err := h.accountService.CreateAccount()
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: accountResp.Result.TrackingId,
utils.DATA_PUBLIC_KEY: accountResp.Result.PublicKey,
utils.DATA_CUSTODIAL_ID: accountResp.Result.CustodialId.String(),
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
okResponse, err := h.accountService.CreateAccount(ctx)
if err != nil {
return err
}
trackingId := okResponse.Result["trackingId"].(string)
publicKey := okResponse.Result["publicKey"].(string)
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: trackingId,
utils.DATA_PUBLIC_KEY: publicKey,
}
store := h.userdataStore
for key, value := range data {
store := h.userdataStore
err := store.WriteEntry(ctx, sessionId, key, []byte(value))
err = store.WriteEntry(ctx, sessionId, key, []byte(value))
if err != nil {
return err
}
}
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
publicKeyNormalized, err := common.NormalizeHex(publicKey)
if err != nil {
return err
}
err = store.WriteEntry(ctx, publicKeyNormalized, utils.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId))
if err != nil {
return err
}
res.FlagSet = append(res.FlagSet, flag_account_created)
return err
return nil
}
@@ -180,34 +204,6 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte)
return res, nil
}
// SavePin persists the user's PIN choice into the filesystem
func (h *Handlers) SavePin(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_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input)
// Validate that the PIN is a 4-digit number
if !isValidPIN(accountPIN) {
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(accountPIN))
if err != nil {
return res, err
}
return res, nil
}
func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
res := resource.Result{}
_, ok := ctx.Value("SessionId").(string)
@@ -226,6 +222,9 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
return res, nil
}
// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_PIN
// during the account creation process
// and during the change PIN process
func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
@@ -234,6 +233,7 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input)
@@ -243,11 +243,15 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN))
if err != nil {
return res, err
}
return res, nil
}
@@ -276,10 +280,10 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
return res, nil
}
// VerifyPin checks whether the confirmation PIN is similar to the account PIN
// If similar, it sets the USERFLAG_PIN_SET flag allowing the user
// VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN
// If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN allowing the user
// to access the main menu
func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
@@ -291,12 +295,12 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN)
temporaryPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN)
if err != nil {
return res, err
}
if bytes.Equal(input, AccountPin) {
if bytes.Equal(input, temporaryPin) {
res.FlagSet = []uint32{flag_valid_pin}
res.FlagReset = []uint32{flag_pin_mismatch}
res.FlagSet = append(res.FlagSet, flag_pin_set)
@@ -304,6 +308,11 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res
res.FlagSet = []uint32{flag_pin_mismatch}
}
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(temporaryPin))
if err != nil {
return res, err
}
return res, nil
}
@@ -326,6 +335,9 @@ func (h *Handlers) SaveFirstname(ctx context.Context, sym string, input []byte)
return res, fmt.Errorf("missing session")
}
if len(input) > 0 {
if bytes.Equal(input, backOption) {
return res, nil
}
firstName := string(input)
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_FIRST_NAME, []byte(firstName))
@@ -345,8 +357,10 @@ func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte)
if !ok {
return res, fmt.Errorf("missing session")
}
if len(input) > 0 {
if bytes.Equal(input, backOption) {
return res, nil
}
familyName := string(input)
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_FAMILY_NAME, []byte(familyName))
@@ -368,7 +382,6 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou
if !ok {
return res, fmt.Errorf("missing session")
}
if len(input) == 4 {
yob := string(input)
store := h.userdataStore
@@ -389,8 +402,10 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) (
if !ok {
return res, fmt.Errorf("missing session")
}
if len(input) > 0 {
if bytes.Equal(input, backOption) {
return res, nil
}
location := string(input)
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_LOCATION, []byte(location))
@@ -411,7 +426,9 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re
if !ok {
return res, fmt.Errorf("missing session")
}
if bytes.Equal(input, backOption) {
return res, nil
}
gender := strings.Split(symbol, "_")[1]
store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
@@ -430,7 +447,6 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte)
if !ok {
return res, fmt.Errorf("missing session")
}
if len(input) > 0 {
offerings := string(input)
store := h.userdataStore
@@ -456,7 +472,6 @@ func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byt
// ResetAccountAuthorized resets the account authorization flag after a successful PIN entry.
func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
res.FlagReset = append(res.FlagReset, flag_account_authorized)
@@ -466,12 +481,10 @@ func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input
// CheckIdentifier retrieves the PublicKey from the JSON data file.
func (h *Handlers) CheckIdentifier(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")
}
store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
@@ -485,12 +498,10 @@ func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte
func (h *Handlers) Authorize(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_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
@@ -542,28 +553,21 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
trackingId, err := store.ReadEntry(ctx, sessionId, utils.DATA_TRACKING_ID)
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, err
}
accountStatus, err := h.accountService.CheckAccountStatus(string(trackingId))
okResponse, err = h.accountService.TrackAccountStatus(ctx, string(publicKey))
if err != nil {
fmt.Println("Error checking account status:", err)
return res, err
}
if !accountStatus.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, err
}
res.FlagReset = append(res.FlagReset, flag_api_error)
status := accountStatus.Result.Transaction.Status
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status))
if err != nil {
return res, nil
isActive := okResponse.Result["active"].(bool)
if !ok {
return res, err
}
if accountStatus.Result.Transaction.Status == "SUCCESS" {
if isActive {
res.FlagSet = append(res.FlagSet, flag_account_success)
res.FlagReset = append(res.FlagReset, flag_account_pending)
} else {
@@ -637,36 +641,41 @@ func (h *Handlers) ResetIncorrectYob(ctx context.Context, sym string, input []by
return res, nil
}
// CheckBalance retrieves the balance from the API using the "PublicKey" and sets
// CheckBalance retrieves the balance of the active voucher and sets
// the balance as the result content
func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
// get the active sym and active balance
activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
if db.IsNotFound(err) {
balance := "0.00"
res.Content = l.Get("Balance: %s\n", balance)
return res, nil
}
return res, err
}
activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil {
return res, err
}
balanceResponse, err := h.accountService.CheckBalance(string(publicKey))
if err != nil {
return res, nil
}
if !balanceResponse.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_error)
balance := balanceResponse.Result.Balance
res.Content = balance
res.Content = l.Get("Balance: %s\n", fmt.Sprintf("%s %s", activeBal, activeSym))
return res, nil
}
@@ -688,7 +697,7 @@ func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input
return res, err
}
balanceResponse, err := h.accountService.CheckBalance(string(publicKey))
balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey))
if err != nil {
return res, nil
}
@@ -804,15 +813,13 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
balanceResp, err := h.accountService.CheckBalance(string(publicKey))
activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil {
return res, nil
return res, err
}
balance := balanceResp.Result.Balance
res.Content = balance
res.Content = string(activeBal)
return res, nil
}
@@ -821,54 +828,29 @@ func (h *Handlers) MaxAmount(ctx context.Context, sym string, input []byte) (res
// it is not more than the current balance.
func (h *Handlers) ValidateAmount(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_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
amountStr := string(input)
var balanceValue float64
balanceRes, err := h.accountService.CheckBalance(string(publicKey))
balanceStr := balanceRes.Result.Balance
if !balanceRes.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
// retrieve the active balance
activeBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL)
if err != nil {
return res, err
}
res.Content = balanceStr
res.FlagReset = append(res.FlagReset, flag_api_error)
// Parse the balance
balanceParts := strings.Split(balanceStr, " ")
if len(balanceParts) != 2 {
return res, fmt.Errorf("unexpected balance format: %s", balanceStr)
}
balanceValue, err := strconv.ParseFloat(balanceParts[0], 64)
balanceValue, err = strconv.ParseFloat(string(activeBal), 64)
if err != nil {
return res, fmt.Errorf("failed to parse balance: %v", err)
return res, err
}
// Extract numeric part from input
re := regexp.MustCompile(`^(\d+(\.\d+)?)\s*(?:CELO)?$`)
matches := re.FindStringSubmatch(strings.TrimSpace(amountStr))
if len(matches) < 2 {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr
return res, nil
}
inputAmount, err := strconv.ParseFloat(matches[1], 64)
// Extract numeric part from the input amount
amountStr := strings.TrimSpace(string(input))
inputAmount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_amount)
res.Content = amountStr
@@ -881,12 +863,14 @@ func (h *Handlers) ValidateAmount(ctx context.Context, sym string, input []byte)
return res, nil
}
res.Content = fmt.Sprintf("%.3f", inputAmount) // Format to 3 decimal places
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(amountStr))
// Format the amount with 2 decimal places before saving
formattedAmount := fmt.Sprintf("%.2f", inputAmount)
err = store.WriteEntry(ctx, sessionId, utils.DATA_AMOUNT, []byte(formattedAmount))
if err != nil {
return res, err
}
res.Content = fmt.Sprintf("%s", formattedAmount)
return res, nil
}
@@ -906,7 +890,7 @@ func (h *Handlers) GetRecipient(ctx context.Context, sym string, input []byte) (
return res, nil
}
// GetSender retrieves the public key from the Gdbm Db
// GetSender returns the sessionId (phoneNumber)
func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
@@ -915,10 +899,7 @@ func (h *Handlers) GetSender(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
res.Content = string(publicKey)
res.Content = string(sessionId)
return res, nil
}
@@ -932,9 +913,16 @@ func (h *Handlers) GetAmount(ctx context.Context, sym string, input []byte) (res
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
// retrieve the active symbol
activeSym, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
return res, err
}
amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT)
res.Content = string(amount)
res.Content = fmt.Sprintf("%s %s", string(amount), string(activeSym))
return res, nil
}
@@ -955,13 +943,14 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input []
// TODO
// Use the amount, recipient and sender to call the API and initialize the transaction
store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
amount, _ := store.ReadEntry(ctx, sessionId, utils.DATA_AMOUNT)
recipient, _ := store.ReadEntry(ctx, sessionId, utils.DATA_RECIPIENT)
res.Content = l.Get("Your request has been sent. %s will receive %s from %s.", string(recipient), string(amount), string(publicKey))
activeSym, _ := store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
res.Content = l.Get("Your request has been sent. %s will receive %s %s from %s.", string(recipient), string(amount), string(activeSym), string(sessionId))
account_authorized_flag, err := h.flagManager.GetFlag("flag_account_authorized")
if err != nil {
@@ -1045,3 +1034,181 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte)
return res, nil
}
// SetDefaultVoucher retrieves the current vouchers
// and sets the first as the default voucher, if no active voucher is set
func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher")
// check if the user has an active sym
_, err = store.ReadEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM)
if err != nil {
if db.IsNotFound(err) {
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, err
}
// Fetch vouchers from the API using the public key
vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil {
return res, err
}
// Return if there is no voucher
if len(vouchersResp.Result.Holdings) == 0 {
res.FlagSet = append(res.FlagSet, flag_no_active_voucher)
return res, nil
}
// Use only the first voucher
firstVoucher := vouchersResp.Result.Holdings[0]
defaultSym := firstVoucher.TokenSymbol
defaultBal := firstVoucher.Balance
// set the active symbol
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(defaultSym))
if err != nil {
return res, err
}
// set the active balance
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(defaultBal))
if err != nil {
return res, err
}
return res, nil
}
return res, err
}
res.FlagReset = append(res.FlagReset, flag_no_active_voucher)
return res, nil
}
// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores
// them to gdbm
func (h *Handlers) CheckVouchers(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")
}
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil {
return res, nil
}
// Fetch vouchers from the API using the public key
vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil {
return res, nil
}
data := utils.ProcessVouchers(vouchersResp.Result.Holdings)
// Store all voucher data
dataMap := map[string]string{
"sym": data.Symbols,
"bal": data.Balances,
"deci": data.Decimals,
"addr": data.Addresses,
}
for key, value := range dataMap {
if err := h.prefixDb.Put(ctx, []byte(key), []byte(value)); err != nil {
return res, nil
}
}
return res, nil
}
// GetVoucherList fetches the list of vouchers and formats them
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
// Read vouchers from the store
voucherData, err := h.prefixDb.Get(ctx, []byte("sym"))
if err != nil {
return res, err
}
res.Content = string(voucherData)
return res, nil
}
// ViewVoucher retrieves the token holding and balance from the subprefixDB
func (h *Handlers) ViewVoucher(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_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
inputStr := string(input)
if inputStr == "0" || inputStr == "99" {
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
return res, nil
}
metadata, err := utils.GetVoucherData(ctx, h.prefixDb, inputStr)
if err != nil {
return res, fmt.Errorf("failed to retrieve voucher data: %v", err)
}
if metadata == nil {
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher)
return res, nil
}
if err := utils.StoreTemporaryVoucher(ctx, h.userdataStore, sessionId, metadata); err != nil {
return res, err
}
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
res.Content = fmt.Sprintf("%s\n%s", metadata.TokenSymbol, metadata.Balance)
return res, nil
}
// SetVoucher retrieves the temp voucher data and sets it as the active data
func (h *Handlers) SetVoucher(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")
}
// Get temporary data
tempData, err := utils.GetTemporaryVoucherData(ctx, h.userdataStore, sessionId)
if err != nil {
return res, err
}
// Set as active and clear temporary data
if err := utils.UpdateVoucherData(ctx, h.userdataStore, sessionId, tempData); err != nil {
return res, err
}
res.Content = tempData.TokenSymbol
return res, nil
}

View File

@@ -15,13 +15,18 @@ import (
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/mocks"
"git.grassecon.net/urdt/ussd/internal/models"
"git.grassecon.net/urdt/ussd/internal/testutil/mocks"
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
"git.grassecon.net/urdt/ussd/internal/utils"
"git.grassecon.net/urdt/ussd/common"
"github.com/alecthomas/assert/v2"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
testdataloader "github.com/peteole/testdata-loader"
"github.com/stretchr/testify/require"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
var (
@@ -31,7 +36,7 @@ var (
func TestNewHandlers(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
accountService := server.TestAccountService{}
accountService := testservice.TestAccountService{}
if err != nil {
t.Logf(err.Error())
}
@@ -72,68 +77,81 @@ func TestCreateAccount(t *testing.T) {
if err != nil {
t.Logf(err.Error())
}
// Create required mocks
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
expectedResult := resource.Result{}
accountCreatedFlag, err := fm.GetFlag("flag_account_created")
flag_account_created, err := fm.GetFlag("flag_account_created")
if err != nil {
t.Logf(err.Error())
}
expectedResult.FlagSet = append(expectedResult.FlagSet, accountCreatedFlag)
// Define session ID and mock data
sessionId := "session123"
typ := utils.DATA_ACCOUNT_CREATED
fakeError := db.ErrNotFound{}
// Create context with session ID
notFoundErr := db.ErrNotFound{}
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
// Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte("123"), fakeError)
expectedAccountResp := &models.AccountResponse{
Ok: true,
Result: struct {
CustodialId json.Number `json:"custodialId"`
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
}{
CustodialId: "12",
PublicKey: "0x8E0XSCSVA",
TrackingId: "d95a7e83-196c-4fd0-866fSGAGA",
tests := []struct {
name string
serverResponse *api.OKResponse
expectedResult resource.Result
}{
{
name: "Test account creation success",
serverResponse: &api.OKResponse{
Ok: true,
Description: "Account creation successed",
Result: map[string]any{
"trackingId": "1234567890",
"publicKey": "0xD3adB33f",
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_account_created},
},
},
}
mockCreateAccountService.On("CreateAccount").Return(expectedAccountResp, nil)
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: expectedAccountResp.Result.TrackingId,
utils.DATA_PUBLIC_KEY: expectedAccountResp.Result.PublicKey,
utils.DATA_CUSTODIAL_ID: expectedAccountResp.Result.CustodialId.String(),
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
// Create a Handlers instance with the mock data store
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
publicKey := tt.serverResponse.Result["publicKey"].(string)
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: tt.serverResponse.Result["trackingId"].(string),
utils.DATA_PUBLIC_KEY: publicKey,
}
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACCOUNT_CREATED).Return([]byte(""), notFoundErr)
mockCreateAccountService.On("CreateAccount").Return(tt.serverResponse, nil)
for key, value := range data {
mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil)
}
publicKeyNormalized, err := common.NormalizeHex(publicKey)
if err != nil {
t.Fatal(err)
}
mockDataStore.On("WriteEntry", ctx, publicKeyNormalized, utils.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId)).Return(nil)
// Call the method you want to test
res, err := h.CreateAccount(ctx, "create_account", []byte("some-input"))
// Assert that no errors occurred
assert.NoError(t, err)
// Assert that the account created flag has been set to the result
assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result")
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
})
}
for key, value := range data {
mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil)
}
// Create a Handlers instance with the mock data store
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
// Call the method you want to test
res, err := h.CreateAccount(ctx, "create_account", []byte("some-input"))
// Assert that no errors occurred
assert.NoError(t, err)
//Assert that the account created flag has been set to the result
assert.Equal(t, res, expectedResult, "Expected result should be equal to the actual result")
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
}
func TestWithPersister(t *testing.T) {
@@ -211,7 +229,7 @@ func TestSaveFamilyname(t *testing.T) {
mockStore.AssertExpectations(t)
}
func TestSavePin(t *testing.T) {
func TestSaveTemporaryPin(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
mockStore := new(mocks.MockUserDataStore)
if err != nil {
@@ -253,10 +271,10 @@ func TestSavePin(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
// Set up the expected behavior of the mock
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(tt.input)).Return(nil)
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(tt.input)).Return(nil)
// Call the method
res, err := h.SavePin(ctx, "save_pin", tt.input)
res, err := h.SaveTemporaryPin(ctx, "save_pin", tt.input)
if err != nil {
t.Error(err)
@@ -474,47 +492,12 @@ func TestCheckIdentifier(t *testing.T) {
}
}
func TestMaxAmount(t *testing.T) {
mockStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
// Define test data
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
publicKey := "0xcasgatweksalw1018221"
expectedBalance := &models.BalanceResponse{
Ok: true,
}
// Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", publicKey).Return(expectedBalance, nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
accountService: mockCreateAccountService,
}
// Call the method
res, _ := h.MaxAmount(ctx, "max_amount", []byte("check_balance"))
//Assert that the balance that was set as the result content is what was returned by Check Balance
assert.Equal(t, expectedBalance.Result.Balance, res.Content)
}
func TestGetSender(t *testing.T) {
mockStore := new(mocks.MockUserDataStore)
// Define test data
sessionId := "session123"
sessionId := "254712345678"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
publicKey := "0xcasgatweksalw1018221"
// Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
// Create the Handlers instance with the mock store
h := &Handlers{
@@ -522,34 +505,37 @@ func TestGetSender(t *testing.T) {
}
// Call the method
res, _ := h.GetSender(ctx, "max_amount", []byte("check_balance"))
//Assert that the public key from readentry operation is what was set as the result content.
assert.Equal(t, publicKey, res.Content)
res, _ := h.GetSender(ctx, "get_sender", []byte(""))
//Assert that the sessionId is what was set as the result content.
assert.Equal(t, sessionId, res.Content)
}
func TestGetAmount(t *testing.T) {
mockStore := new(mocks.MockUserDataStore)
mockDataStore := new(mocks.MockUserDataStore)
// Define test data
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
Amount := "0.03CELO"
amount := "0.03"
activeSym := "SRF"
// Set up the expected behavior of the mock
mockStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return([]byte(Amount), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(activeSym), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return([]byte(amount), nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
userdataStore: mockDataStore,
}
// Call the method
res, _ := h.GetAmount(ctx, "get_amount", []byte("Getting amount..."))
res, _ := h.GetAmount(ctx, "get_amount", []byte(""))
formattedAmount := fmt.Sprintf("%s %s", amount, activeSym)
//Assert that the retrieved amount is what was set as the content
assert.Equal(t, Amount, res.Content)
assert.Equal(t, formattedAmount, res.Content)
}
@@ -978,7 +964,7 @@ func TestVerifyYob(t *testing.T) {
}
}
func TestVerifyPin(t *testing.T) {
func TestVerifyCreatePin(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
@@ -1027,7 +1013,7 @@ func TestVerifyPin(t *testing.T) {
},
}
typ := utils.DATA_ACCOUNT_PIN
typ := utils.DATA_TEMPORARY_PIN
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1035,8 +1021,11 @@ func TestVerifyPin(t *testing.T) {
// Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte(firstSetPin), nil)
// Set up the expected behavior of the mock
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(firstSetPin)).Return(nil)
// Call the method under test
res, err := h.VerifyPin(ctx, "verify_pin", []byte(tt.input))
res, err := h.VerifyCreatePin(ctx, "verify_create_pin", []byte(tt.input))
// Assert that no errors occurred
assert.NoError(t, err)
@@ -1066,12 +1055,20 @@ func TestCheckAccountStatus(t *testing.T) {
tests := []struct {
name string
input []byte
serverResponse *api.OKResponse
response *models.TrackStatusResponse
expectedResult resource.Result
}{
{
name: "Test when account status is Success",
name: "Test when account is on the Sarafu network",
input: []byte("TrackingId1234"),
serverResponse: &api.OKResponse{
Ok: true,
Description: "Account creation succeeded",
Result: map[string]any{
"active": true,
},
},
response: &models.TrackStatusResponse{
Ok: true,
Result: struct {
@@ -1098,17 +1095,7 @@ func TestCheckAccountStatus(t *testing.T) {
},
},
{
name: "Test when fetching account status is not Success",
input: []byte("TrackingId1234"),
response: &models.TrackStatusResponse{
Ok: false,
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_api_error},
},
},
{
name: "Test when checking account status api call is a SUCCESS but an account is not yet ready",
name: "Test when the account is not yet on the sarafu network",
input: []byte("TrackingId1234"),
response: &models.TrackStatusResponse{
Ok: true,
@@ -1123,13 +1110,20 @@ func TestCheckAccountStatus(t *testing.T) {
}{
Transaction: models.Transaction{
CreatedAt: time.Now(),
Status: "IN_NETWORK",
Status: "SUCCESS",
TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def",
TxType: "transfer",
},
},
},
serverResponse: &api.OKResponse{
Ok: true,
Description: "Account creation succeeded",
Result: map[string]any{
"active": false,
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_account_pending},
FlagReset: []uint32{flag_api_error, flag_account_success},
@@ -1149,9 +1143,10 @@ func TestCheckAccountStatus(t *testing.T) {
status := tt.response.Result.Transaction.Status
// Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TRACKING_ID).Return(tt.input, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.input, nil)
mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.response, nil)
mockCreateAccountService.On("TrackAccountStatus", string(tt.input)).Return(tt.serverResponse, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)).Return(nil).Maybe()
// Call the method under test
@@ -1284,7 +1279,7 @@ func TestResetInvalidAmount(t *testing.T) {
}
func TestInitiateTransaction(t *testing.T) {
sessionId := "session123"
sessionId := "254712345678"
fm, err := NewFlagManager(flagsPath)
@@ -1307,30 +1302,29 @@ func TestInitiateTransaction(t *testing.T) {
tests := []struct {
name string
input []byte
PublicKey []byte
Recipient []byte
Amount []byte
ActiveSym []byte
status string
expectedResult resource.Result
}{
{
name: "Test amount reset",
PublicKey: []byte("0x1241527192"),
Amount: []byte("0.002CELO"),
name: "Test initiate transaction",
Amount: []byte("0.002"),
ActiveSym: []byte("SRF"),
Recipient: []byte("0x12415ass27192"),
expectedResult: resource.Result{
FlagReset: []uint32{account_authorized_flag},
Content: "Your request has been sent. 0x12415ass27192 will receive 0.002CELO from 0x1241527192.",
Content: "Your request has been sent. 0x12415ass27192 will receive 0.002 SRF from 254712345678.",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.PublicKey, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_AMOUNT).Return(tt.Amount, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_RECIPIENT).Return(tt.Recipient, nil)
//mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, []byte("")).Return(nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return(tt.ActiveSym, nil)
// Call the method under test
res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input)
@@ -1343,10 +1337,8 @@ func TestInitiateTransaction(t *testing.T) {
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
})
}
}
func TestQuit(t *testing.T) {
@@ -1462,7 +1454,6 @@ func TestValidateAmount(t *testing.T) {
t.Logf(err.Error())
}
flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount")
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
@@ -1476,92 +1467,59 @@ func TestValidateAmount(t *testing.T) {
flagManager: fm.parser,
}
tests := []struct {
name string
input []byte
publicKey []byte
balanceResponse *models.BalanceResponse
expectedResult resource.Result
name string
input []byte
activeBal []byte
balance string
expectedResult resource.Result
}{
{
name: "Test with valid amount",
input: []byte("0.001"),
balanceResponse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
name: "Test with valid amount",
input: []byte("4.10"),
activeBal: []byte("5"),
expectedResult: resource.Result{
Content: "0.001",
FlagReset: []uint32{flag_api_error},
Content: "4.10",
},
},
{
name: "Test with amount larger than balance",
input: []byte("0.02"),
balanceResponse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
name: "Test with amount larger than active balance",
input: []byte("5.02"),
activeBal: []byte("5"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount},
FlagReset: []uint32{flag_api_error},
Content: "0.02",
FlagSet: []uint32{flag_invalid_amount},
Content: "5.02",
},
},
{
name: "Test with invalid amount",
input: []byte("0.02ms"),
balanceResponse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
publicKey: []byte("0xrqeqrequuq"),
name: "Test with invalid amount format",
input: []byte("0.02ms"),
activeBal: []byte("5"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_amount},
FlagReset: []uint32{flag_api_error},
Content: "0.02ms",
FlagSet: []uint32{flag_invalid_amount},
Content: "0.02ms",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Mock behavior for active balance retrieval
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL).Return(tt.activeBal, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil)
mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balanceResponse, nil)
// Mock behavior for storing the amount (if valid)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe()
// Call the method under test
res, _ := h.ValidateAmount(ctx, "test_validate_amount", tt.input)
// Assert that no errors occurred
// Assert no errors occurred
assert.NoError(t, err)
//Assert that the account created flag has been set to the result
assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result")
// Assert the result matches the expected result
assert.Equal(t, tt.expectedResult, res, "Expected result should match actual result")
// Assert that expectations were met
// Assert all expectations were met
mockDataStore.AssertExpectations(t)
})
}
}
@@ -1625,85 +1583,54 @@ func TestValidateRecipient(t *testing.T) {
}
func TestCheckBalance(t *testing.T) {
sessionId := "session123"
publicKey := "0X13242618721"
fm, _ := NewFlagManager(flagsPath)
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct {
name string
balanceResonse *models.BalanceResponse
sessionId string
publicKey string
activeSym string
activeBal string
expectedResult resource.Result
expectError bool
}{
{
name: "Test when checking a balance is not a success",
balanceResonse: &models.BalanceResponse{
Ok: false,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_api_error},
},
},
{
name: "Test when checking a balance is a success",
balanceResonse: &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
},
expectedResult: resource.Result{
Content: "0.003 CELO",
FlagReset: []uint32{flag_api_error},
},
name: "User with active sym",
sessionId: "session456",
publicKey: "0X98765432109",
activeSym: "ETH",
activeBal: "1.5",
expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockCreateAccountService := new(mocks.MockAccountService)
mockState := state.NewState(16)
mockAccountService := new(mocks.MockAccountService)
ctx := context.WithValue(context.Background(), "SessionId", tt.sessionId)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockDataStore,
flagManager: fm.parser,
st: mockState,
accountService: mockCreateAccountService,
accountService: mockAccountService,
}
// Set up the expected behavior of the mock
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResonse, nil)
// Mock for user with active sym
mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(tt.activeSym), nil)
mockDataStore.On("ReadEntry", ctx, tt.sessionId, utils.DATA_ACTIVE_BAL).Return([]byte(tt.activeBal), nil)
// Call the method
res, _ := h.CheckBalance(ctx, "check_balance", []byte(""))
res, err := h.CheckBalance(ctx, "check_balance", []byte(""))
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, res, "Result should match expected output")
}
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
//Assert that the result set to content is what was expected
assert.Equal(t, res, tt.expectedResult, "Result should contain flags set according to user input")
mockAccountService.AssertExpectations(t)
})
}
}
func TestGetProfile(t *testing.T) {
@@ -1845,42 +1772,6 @@ func TestVerifyNewPin(t *testing.T) {
}
func TestSaveTemporaryPIn(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
// Create a new instance of UserDataStore
mockStore := new(mocks.MockUserDataStore)
// Define test data
sessionId := "session123"
PIN := "1234"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
// Set up the expected behavior of the mock
mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(PIN)).Return(nil)
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: mockStore,
flagManager: fm.parser,
}
// Call the method
res, err := h.SaveTemporaryPin(ctx, "save_temporary_pin", []byte(PIN))
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
// Assert all expectations were met
mockStore.AssertExpectations(t)
}
func TestConfirmPin(t *testing.T) {
sessionId := "session123"
@@ -1928,7 +1819,6 @@ func TestConfirmPin(t *testing.T) {
})
}
}
func TestFetchCustodialBalances(t *testing.T) {
@@ -2012,3 +1902,256 @@ func TestFetchCustodialBalances(t *testing.T) {
})
}
}
func TestSetDefaultVoucher(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
flag_no_active_voucher, err := fm.GetFlag("flag_no_active_voucher")
if err != nil {
t.Logf(err.Error())
}
// Define session ID and mock data
sessionId := "session123"
publicKey := "0X13242618721"
notFoundErr := db.ErrNotFound{}
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
tests := []struct {
name string
vouchersResp *models.VoucherHoldingResponse
expectedResult resource.Result
}{
{
name: "Test set default voucher when no active voucher exists",
vouchersResp: &models.VoucherHoldingResponse{
Ok: true,
Description: "Vouchers fetched successfully",
Result: models.VoucherResult{
Holdings: []dataserviceapi.TokenHoldings{
{
ContractAddress: "0x123",
TokenSymbol: "TOKEN1",
TokenDecimals: "18",
Balance: "100",
},
},
},
},
expectedResult: resource.Result{},
},
{
name: "Test no vouchers available",
vouchersResp: &models.VoucherHoldingResponse{
Ok: true,
Description: "No vouchers available",
Result: models.VoucherResult{
Holdings: []dataserviceapi.TokenHoldings{},
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_no_active_voucher},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockAccountService := new(mocks.MockAccountService)
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockAccountService,
flagManager: fm.parser,
}
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM).Return([]byte(""), notFoundErr)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockAccountService.On("FetchVouchers", string(publicKey)).Return(tt.vouchersResp, nil)
if len(tt.vouchersResp.Result.Holdings) > 0 {
firstVoucher := tt.vouchersResp.Result.Holdings[0]
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(firstVoucher.TokenSymbol)).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(firstVoucher.Balance)).Return(nil)
}
res, err := h.SetDefaultVoucher(ctx, "set_default_voucher", []byte("some-input"))
assert.NoError(t, err)
assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result")
mockDataStore.AssertExpectations(t)
mockAccountService.AssertExpectations(t)
})
}
}
func TestCheckVouchers(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
mockAccountService := new(mocks.MockAccountService)
mockSubPrefixDb := new(mocks.MockSubPrefixDb)
sessionId := "session123"
publicKey := "0X13242618721"
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockAccountService,
prefixDb: mockSubPrefixDb,
}
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockVouchersResponse := &models.VoucherHoldingResponse{}
mockVouchersResponse.Result.Holdings = []dataserviceapi.TokenHoldings{
{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100"},
{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200"},
}
mockAccountService.On("FetchVouchers", string(publicKey)).Return(mockVouchersResponse, nil)
mockSubPrefixDb.On("Put", ctx, []byte("sym"), []byte("1:SRF\n2:MILO")).Return(nil)
mockSubPrefixDb.On("Put", ctx, []byte("bal"), []byte("1:100\n2:200")).Return(nil)
mockSubPrefixDb.On("Put", ctx, []byte("deci"), []byte("1:6\n2:4")).Return(nil)
mockSubPrefixDb.On("Put", ctx, []byte("addr"), []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa")).Return(nil)
_, err := h.CheckVouchers(ctx, "check_vouchers", []byte(""))
assert.NoError(t, err)
mockDataStore.AssertExpectations(t)
mockAccountService.AssertExpectations(t)
}
func TestGetVoucherList(t *testing.T) {
mockSubPrefixDb := new(mocks.MockSubPrefixDb)
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
h := &Handlers{
prefixDb: mockSubPrefixDb,
}
mockSubPrefixDb.On("Get", ctx, []byte("sym")).Return([]byte("1:SRF\n2:MILO"), nil)
res, err := h.GetVoucherList(ctx, "", []byte(""))
assert.NoError(t, err)
assert.Contains(t, res.Content, "1:SRF\n2:MILO")
mockSubPrefixDb.AssertExpectations(t)
}
func TestViewVoucher(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
mockDataStore := new(mocks.MockUserDataStore)
mockSubPrefixDb := new(mocks.MockSubPrefixDb)
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
h := &Handlers{
userdataStore: mockDataStore,
flagManager: fm.parser,
prefixDb: mockSubPrefixDb,
}
// Define mock voucher data
mockVoucherData := map[string]string{
"sym": "1:SRF",
"bal": "1:100",
"deci": "1:6",
"addr": "1:0xd4c288865Ce",
}
for key, value := range mockVoucherData {
mockSubPrefixDb.On("Get", ctx, []byte(key)).Return([]byte(value), nil)
}
// Set up expectations for mockDataStore
expectedData := map[utils.DataTyp]string{
utils.DATA_TEMPORARY_SYM: "SRF",
utils.DATA_TEMPORARY_BAL: "100",
utils.DATA_TEMPORARY_DECIMAL: "6",
utils.DATA_TEMPORARY_ADDRESS: "0xd4c288865Ce",
}
for dataType, dataValue := range expectedData {
mockDataStore.On("WriteEntry", ctx, sessionId, dataType, []byte(dataValue)).Return(nil)
}
res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1"))
assert.NoError(t, err)
assert.Contains(t, res.Content, "SRF\n100")
mockDataStore.AssertExpectations(t)
mockSubPrefixDb.AssertExpectations(t)
}
func TestSetVoucher(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
// Define the temporary voucher data
tempData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Define the expected active entries
activeEntries := map[utils.DataTyp][]byte{
utils.DATA_ACTIVE_SYM: []byte(tempData.TokenSymbol),
utils.DATA_ACTIVE_BAL: []byte(tempData.Balance),
utils.DATA_ACTIVE_DECIMAL: []byte(tempData.TokenDecimals),
utils.DATA_ACTIVE_ADDRESS: []byte(tempData.ContractAddress),
}
// Define the temporary entries to be cleared
tempEntries := map[utils.DataTyp][]byte{
utils.DATA_TEMPORARY_SYM: []byte(""),
utils.DATA_TEMPORARY_BAL: []byte(""),
utils.DATA_TEMPORARY_DECIMAL: []byte(""),
utils.DATA_TEMPORARY_ADDRESS: []byte(""),
}
// Mocking ReadEntry calls for temporary data retrieval
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_SYM).Return([]byte(tempData.TokenSymbol), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_BAL).Return([]byte(tempData.Balance), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_DECIMAL).Return([]byte(tempData.TokenDecimals), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_ADDRESS).Return([]byte(tempData.ContractAddress), nil)
// Mocking WriteEntry calls for setting active data
for key, value := range activeEntries {
mockDataStore.On("WriteEntry", ctx, sessionId, key, value).Return(nil)
}
// Mocking WriteEntry calls for clearing temporary data
for key, value := range tempEntries {
mockDataStore.On("WriteEntry", ctx, sessionId, key, value).Return(nil)
}
h := &Handlers{
userdataStore: mockDataStore,
}
res, err := h.SetVoucher(ctx, "someSym", []byte{})
assert.NoError(t, err)
assert.Equal(t, string(tempData.TokenSymbol), res.Content)
mockDataStore.AssertExpectations(t)
}

View File

@@ -13,7 +13,7 @@ import (
"git.defalsify.org/vise.git/engine"
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/mocks/httpmocks"
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
)
// invalidRequestType is a custom type to test invalid request scenarios

View File

@@ -1,26 +0,0 @@
package mocks
import (
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/stretchr/testify/mock"
)
// MockAccountService implements AccountServiceInterface for testing
type MockAccountService struct {
mock.Mock
}
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) {
args := m.Called()
return args.Get(0).(*models.AccountResponse), args.Error(1)
}
func (m *MockAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.BalanceResponse), args.Error(1)
}
func (m *MockAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
args := m.Called(trackingId)
return args.Get(0).(*models.TrackStatusResponse), args.Error(1)
}

View File

@@ -1,15 +1,10 @@
package models
import (
"encoding/json"
)
type AccountResponse struct {
Ok bool `json:"ok"`
Result struct {
CustodialId json.Number `json:"custodialId"`
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
Ok bool `json:"ok"`
Description string `json:"description"` // Include the description field
Result struct {
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
} `json:"result"`
}
}

View File

@@ -0,0 +1,18 @@
package models
type ApiResponse struct {
OK bool `json:"ok"`
Description string `json:"description"`
Result Result `json:"result"`
}
type Result struct {
Holdings []Holding `json:"holdings"`
}
type Holding struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}

View File

@@ -4,7 +4,6 @@ import (
"encoding/json"
"time"
)
type Transaction struct {
CreatedAt time.Time `json:"createdAt"`
Status string `json:"status"`

View File

@@ -0,0 +1,14 @@
package models
import dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
type VoucherHoldingResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result VoucherResult `json:"result"`
}
// VoucherResult holds the list of token holdings
type VoucherResult struct {
Holdings []dataserviceapi.TokenHoldings `json:"holdings"`
}

View File

@@ -10,34 +10,38 @@ const (
DATATYPE_USERSUB = 64
)
// PrefixDb interface abstracts the database operations.
type PrefixDb interface {
Get(ctx context.Context, key []byte) ([]byte, error)
Put(ctx context.Context, key []byte, val []byte) error
}
var _ PrefixDb = (*SubPrefixDb)(nil)
type SubPrefixDb struct {
store db.Db
pfx []byte
pfx []byte
}
func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
return &SubPrefixDb{
store: store,
pfx: pfx,
pfx: pfx,
}
}
func(s *SubPrefixDb) toKey(k []byte) []byte {
return append(s.pfx, k...)
func (s *SubPrefixDb) toKey(k []byte) []byte {
return append(s.pfx, k...)
}
func(s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(DATATYPE_USERSUB)
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
v, err := s.store.Get(ctx, key)
if err != nil {
return nil, err
}
return v, nil
return s.store.Get(ctx, key)
}
func(s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
s.store.SetPrefix(DATATYPE_USERSUB)
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key)
return s.store.Put(ctx, key, val)
return s.store.Put(ctx, key, val)
}

View File

@@ -13,6 +13,8 @@ import (
"git.grassecon.net/urdt/ussd/internal/handlers"
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
"git.grassecon.net/urdt/ussd/internal/testutil/testtag"
testdataloader "github.com/peteole/testdata-loader"
)
@@ -80,12 +82,12 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
os.Exit(1)
}
if AccountService == nil {
AccountService = &server.AccountService{}
if testtag.AccountService == nil {
testtag.AccountService = &server.AccountService{}
}
switch AccountService.(type) {
case *server.TestAccountService:
switch testtag.AccountService.(type) {
case *testservice.TestAccountService:
go func() {
eventChannel <- false
}()
@@ -98,7 +100,7 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
panic("Unknown account service type")
}
hl, err := lhs.GetHandler(AccountService)
hl, err := lhs.GetHandler(testtag.AccountService)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)

View File

@@ -0,0 +1,40 @@
package mocks
import (
"context"
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
"github.com/stretchr/testify/mock"
)
// MockAccountService implements AccountServiceInterface for testing
type MockAccountService struct {
mock.Mock
}
func (m *MockAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
args := m.Called()
return args.Get(0).(*api.OKResponse), args.Error(1)
}
func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.BalanceResponse), args.Error(1)
}
func (m *MockAccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) {
args := m.Called(trackingId)
return args.Get(0).(*models.TrackStatusResponse), args.Error(1)
}
func (m *MockAccountService) TrackAccountStatus(ctx context.Context,publicKey string) (*api.OKResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*api.OKResponse), args.Error(1)
}
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.VoucherHoldingResponse), args.Error(1)
}

View File

@@ -0,0 +1,21 @@
package mocks
import (
"context"
"github.com/stretchr/testify/mock"
)
type MockSubPrefixDb struct {
mock.Mock
}
func (m *MockSubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
args := m.Called(ctx, key)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockSubPrefixDb) Put(ctx context.Context, key, val []byte) error {
args := m.Called(ctx, key, val)
return args.Error(0)
}

View File

@@ -1,11 +0,0 @@
// +build !online
package testutil
import (
"git.grassecon.net/urdt/ussd/internal/handlers/server"
)
var (
AccountService server.AccountServiceInterface = &server.TestAccountService{}
)

View File

@@ -0,0 +1,90 @@
package testservice
import (
"context"
"encoding/json"
"time"
"git.grassecon.net/urdt/ussd/internal/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
type TestAccountService struct {
}
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
return &api.OKResponse{
Ok: true,
Description: "Account creation succeeded",
Result: map[string]any{
"trackingId": "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d",
"publicKey": "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD",
},
}, nil
}
func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) {
balanceResponse := &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
}
return balanceResponse, nil
}
func (tas *TestAccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) {
trackResponse := &models.TrackStatusResponse{
Ok: true,
Result: struct {
Transaction struct {
CreatedAt time.Time "json:\"createdAt\""
Status string "json:\"status\""
TransferValue json.Number "json:\"transferValue\""
TxHash string "json:\"txHash\""
TxType string "json:\"txType\""
}
}{
Transaction: models.Transaction{
CreatedAt: time.Now(),
Status: "SUCCESS",
TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def",
TxType: "transfer",
},
},
}
return trackResponse, nil
}
func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) {
return &api.OKResponse{
Ok: true,
Description: "Account creation succeeded",
Result: map[string]any{
"active": true,
},
}, nil
}
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
return &models.VoucherHoldingResponse{
Ok: true,
Result: models.VoucherResult{
Holdings: []dataserviceapi.TokenHoldings{
{
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF",
TokenDecimals: "6",
Balance: "2745987",
},
},
},
}, nil
}

View File

@@ -0,0 +1,12 @@
// +build !online
package testtag
import (
"git.grassecon.net/urdt/ussd/internal/handlers/server"
accountservice "git.grassecon.net/urdt/ussd/internal/testutil/testservice"
)
var (
AccountService server.AccountServiceInterface = &accountservice.TestAccountService{}
)

View File

@@ -1,6 +1,6 @@
// +build online
package testutil
package testtag
import "git.grassecon.net/urdt/ussd/internal/handlers/server"

View File

@@ -23,6 +23,16 @@ const (
DATA_RECIPIENT
DATA_AMOUNT
DATA_TEMPORARY_PIN
DATA_VOUCHER_LIST
DATA_TEMPORARY_SYM
DATA_ACTIVE_SYM
DATA_TEMPORARY_BAL
DATA_ACTIVE_BAL
DATA_PUBLIC_KEY_REVERSE
DATA_TEMPORARY_DECIMAL
DATA_ACTIVE_DECIMAL
DATA_TEMPORARY_ADDRESS
DATA_ACTIVE_ADDRESS
)
func typToBytes(typ DataTyp) []byte {

161
internal/utils/vouchers.go Normal file
View File

@@ -0,0 +1,161 @@
package utils
import (
"context"
"fmt"
"strings"
"git.grassecon.net/urdt/ussd/internal/storage"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// VoucherMetadata helps organize data fields
type VoucherMetadata struct {
Symbols string
Balances string
Decimals string
Addresses string
}
// ProcessVouchers converts holdings into formatted strings
func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
var data VoucherMetadata
var symbols, balances, decimals, addresses []string
for i, h := range holdings {
symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol))
balances = append(balances, fmt.Sprintf("%d:%s", i+1, h.Balance))
decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals))
addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress))
}
data.Symbols = strings.Join(symbols, "\n")
data.Balances = strings.Join(balances, "\n")
data.Decimals = strings.Join(decimals, "\n")
data.Addresses = strings.Join(addresses, "\n")
return data
}
// GetVoucherData retrieves and matches voucher data
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
keys := []string{"sym", "bal", "deci", "addr"}
data := make(map[string]string)
for _, key := range keys {
value, err := db.Get(ctx, []byte(key))
if err != nil {
return nil, fmt.Errorf("failed to get %s: %v", key, err)
}
data[key] = string(value)
}
symbol, balance, decimal, address := MatchVoucher(input,
data["sym"],
data["bal"],
data["deci"],
data["addr"])
if symbol == "" {
return nil, nil
}
return &dataserviceapi.TokenHoldings{
TokenSymbol: string(symbol),
Balance: string(balance),
TokenDecimals: string(decimal),
ContractAddress: string(address),
}, nil
}
// MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input.
func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) {
symList := strings.Split(symbols, "\n")
balList := strings.Split(balances, "\n")
decList := strings.Split(decimals, "\n")
addrList := strings.Split(addresses, "\n")
for i, sym := range symList {
parts := strings.SplitN(sym, ":", 2)
if input == parts[0] || strings.EqualFold(input, parts[1]) {
symbol = parts[1]
if i < len(balList) {
balance = strings.SplitN(balList[i], ":", 2)[1]
}
if i < len(decList) {
decimal = strings.SplitN(decList[i], ":", 2)[1]
}
if i < len(addrList) {
address = strings.SplitN(addrList[i], ":", 2)[1]
}
break
}
}
return
}
// StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore.
func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
entries := map[DataTyp][]byte{
DATA_TEMPORARY_SYM: []byte(data.TokenSymbol),
DATA_TEMPORARY_BAL: []byte(data.Balance),
DATA_TEMPORARY_DECIMAL: []byte(data.TokenDecimals),
DATA_TEMPORARY_ADDRESS: []byte(data.ContractAddress),
}
for key, value := range entries {
if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
return err
}
}
return nil
}
// GetTemporaryVoucherData retrieves temporary voucher metadata from the DataStore.
func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
keys := []DataTyp{
DATA_TEMPORARY_SYM,
DATA_TEMPORARY_BAL,
DATA_TEMPORARY_DECIMAL,
DATA_TEMPORARY_ADDRESS,
}
data := &dataserviceapi.TokenHoldings{}
values := make([][]byte, len(keys))
for i, key := range keys {
value, err := store.ReadEntry(ctx, sessionId, key)
if err != nil {
return nil, err
}
values[i] = value
}
data.TokenSymbol = string(values[0])
data.Balance = string(values[1])
data.TokenDecimals = string(values[2])
data.ContractAddress = string(values[3])
return data, nil
}
// UpdateVoucherData sets the active voucher data in the DataStore.
func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
// Active voucher data entries
activeEntries := map[DataTyp][]byte{
DATA_ACTIVE_SYM: []byte(data.TokenSymbol),
DATA_ACTIVE_BAL: []byte(data.Balance),
DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals),
DATA_ACTIVE_ADDRESS: []byte(data.ContractAddress),
}
// Write active data
for key, value := range activeEntries {
if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,203 @@
package utils
import (
"context"
"testing"
"git.grassecon.net/urdt/ussd/internal/storage"
"github.com/alecthomas/assert/v2"
"github.com/stretchr/testify/require"
memdb "git.defalsify.org/vise.git/db/mem"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// InitializeTestDb sets up and returns an in-memory database and store.
func InitializeTestDb(t *testing.T) (context.Context, *UserDataStore) {
ctx := context.Background()
// Initialize memDb
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
require.NoError(t, err, "Failed to connect to memDb")
// Create UserDataStore with memDb
store := &UserDataStore{Db: db}
t.Cleanup(func() {
db.Close() // Ensure the DB is closed after each test
})
return ctx, store
}
func TestMatchVoucher(t *testing.T) {
symbols := "1:SRF\n2:MILO"
balances := "1:100\n2:200"
decimals := "1:6\n2:4"
addresses := "1:0xd4c288865Ce\n2:0x41c188d63Qa"
// Test for valid voucher
symbol, balance, decimal, address := MatchVoucher("2", symbols, balances, decimals, addresses)
// Assertions for valid voucher
assert.Equal(t, "MILO", symbol)
assert.Equal(t, "200", balance)
assert.Equal(t, "4", decimal)
assert.Equal(t, "0x41c188d63Qa", address)
// Test for non-existent voucher
symbol, balance, decimal, address = MatchVoucher("3", symbols, balances, decimals, addresses)
// Assertions for non-match
assert.Equal(t, "", symbol)
assert.Equal(t, "", balance)
assert.Equal(t, "", decimal)
assert.Equal(t, "", address)
}
func TestProcessVouchers(t *testing.T) {
holdings := []dataserviceapi.TokenHoldings{
{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100"},
{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200"},
}
expectedResult := VoucherMetadata{
Symbols: "1:SRF\n2:MILO",
Balances: "1:100\n2:200",
Decimals: "1:6\n2:4",
Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa",
}
result := ProcessVouchers(holdings)
assert.Equal(t, expectedResult, result)
}
func TestGetVoucherData(t *testing.T) {
ctx := context.Background()
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
if err != nil {
t.Fatal(err)
}
spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
// Test voucher data
mockData := map[string][]byte{
"sym": []byte("1:SRF\n2:MILO"),
"bal": []byte("1:100\n2:200"),
"deci": []byte("1:6\n2:4"),
"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
}
// Put the data
for key, value := range mockData {
err = spdb.Put(ctx, []byte(key), []byte(value))
if err != nil {
t.Fatal(err)
}
}
result, err := GetVoucherData(ctx, spdb, "1")
assert.NoError(t, err)
assert.Equal(t, "SRF", result.TokenSymbol)
assert.Equal(t, "100", result.Balance)
assert.Equal(t, "6", result.TokenDecimals)
assert.Equal(t, "0xd4c288865Ce", result.ContractAddress)
}
func TestStoreTemporaryVoucher(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// Test data
voucherData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Execute the function being tested
err := StoreTemporaryVoucher(ctx, store, sessionId, voucherData)
require.NoError(t, err)
// Verify stored data
expectedEntries := map[DataTyp][]byte{
DATA_TEMPORARY_SYM: []byte("SRF"),
DATA_TEMPORARY_BAL: []byte("200"),
DATA_TEMPORARY_DECIMAL: []byte("6"),
DATA_TEMPORARY_ADDRESS: []byte("0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9"),
}
for key, expectedValue := range expectedEntries {
storedValue, err := store.ReadEntry(ctx, sessionId, key)
require.NoError(t, err)
require.Equal(t, expectedValue, storedValue, "Mismatch for key %v", key)
}
}
func TestGetTemporaryVoucherData(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// Test voucher data
tempData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Store the data
err := StoreTemporaryVoucher(ctx, store, sessionId, tempData)
require.NoError(t, err)
// Execute the function being tested
data, err := GetTemporaryVoucherData(ctx, store, sessionId)
require.NoError(t, err)
require.Equal(t, tempData, data)
}
func TestUpdateVoucherData(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// New voucher data
newData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Old temporary data
tempData := &dataserviceapi.TokenHoldings{
TokenSymbol: "OLD",
Balance: "100",
TokenDecimals: "8",
ContractAddress: "0xold",
}
require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData))
// Execute update
err := UpdateVoucherData(ctx, store, sessionId, newData)
require.NoError(t, err)
// Verify active data was stored correctly
activeEntries := map[DataTyp][]byte{
DATA_ACTIVE_SYM: []byte(newData.TokenSymbol),
DATA_ACTIVE_BAL: []byte(newData.Balance),
DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals),
DATA_ACTIVE_ADDRESS: []byte(newData.ContractAddress),
}
for key, expectedValue := range activeEntries {
storedValue, err := store.ReadEntry(ctx, sessionId, key)
require.NoError(t, err)
require.Equal(t, expectedValue, storedValue, "Active data mismatch for key %v", key)
}
}

View File

@@ -5,7 +5,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -33,7 +33,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@@ -42,7 +42,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -70,7 +70,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@@ -79,7 +79,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -116,7 +116,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@@ -125,7 +125,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -162,7 +162,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@@ -171,7 +171,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -203,7 +203,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@@ -212,7 +212,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -244,7 +244,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
@@ -254,7 +254,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -286,7 +286,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@@ -295,7 +295,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -327,7 +327,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@@ -336,7 +336,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -368,7 +368,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@@ -377,7 +377,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -409,7 +409,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
},
@@ -418,7 +418,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
@@ -446,7 +446,7 @@
},
{
"input": "0",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}

View File

@@ -9,8 +9,8 @@ import (
"regexp"
"testing"
"git.grassecon.net/urdt/ussd/driver"
"git.grassecon.net/urdt/ussd/internal/testutil"
"git.grassecon.net/urdt/ussd/internal/testutil/driver"
"github.com/gofrs/uuid"
)
@@ -43,6 +43,39 @@ func extractPublicKey(response []byte) string {
return ""
}
// Extracts the balance value from the engine response.
func extractBalance(response []byte) string {
// Regex to match "Balance: <amount> <symbol>" followed by a newline
re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
}
return ""
}
// Extracts the Maximum amount value from the engine response.
func extractMaxAmount(response []byte) string {
// Regex to match "Maximum amount: <amount>" followed by a newline
re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) // "<amount>"
}
return ""
}
// Extracts the send amount value from the engine response.
func extractSendAmount(response []byte) string {
// Regex to match the pattern "will receive X.XX SYM from"
re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`)
match := re.FindSubmatch(response)
if match != nil {
return string(match[1]) // Returns "X.XX SYM"
}
return ""
}
func TestMain(m *testing.M) {
sessionID = GenerateSessionId()
defer func() {
@@ -154,6 +187,12 @@ func TestMainMenuHelp(t *testing.T) {
}
b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
@@ -189,6 +228,12 @@ func TestMainMenuQuit(t *testing.T) {
}
b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
@@ -225,8 +270,13 @@ func TestMyAccount_MyAddress(t *testing.T) {
}
b := w.Bytes()
balance := extractBalance(b)
publicKey := extractPublicKey(b)
expectedContent := bytes.Replace([]byte(step.ExpectedContent), []byte("{public_key}"), []byte(publicKey), -1)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
@@ -240,6 +290,52 @@ func TestMyAccount_MyAddress(t *testing.T) {
}
}
func TestMainMenuSend(t *testing.T) {
en, fn, _ := testutil.TestEngine(sessionID)
defer fn()
ctx := context.Background()
sessions := testData
for _, session := range sessions {
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
for _, group := range groups {
for _, step := range group.Steps {
cont, err := en.Exec(ctx, []byte(step.Input))
if err != nil {
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
return
}
if !cont {
break
}
w := bytes.NewBuffer(nil)
if _, err := en.Flush(ctx, w); err != nil {
t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
max_amount := extractMaxAmount(b)
send_amount := extractSendAmount(b)
expectedContent := []byte(step.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1)
expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1)
step.ExpectedContent = string(expectedContent)
match, err := step.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err)
}
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b)
}
}
}
}
}
func TestGroups(t *testing.T) {
groups, err := driver.LoadTestGroups(groupTestFile)
if err != nil {
@@ -265,6 +361,13 @@ func TestGroups(t *testing.T) {
t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err)
}
b := w.Bytes()
balance := extractBalance(b)
expectedContent := []byte(tt.ExpectedContent)
expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1)
tt.ExpectedContent = string(expectedContent)
match, err := tt.MatchesExpectedContent(b)
if err != nil {
t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err)
@@ -272,7 +375,6 @@ func TestGroups(t *testing.T) {
if !match {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b)
}
})
}
}

View File

@@ -57,7 +57,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "1",
@@ -73,19 +73,19 @@
},
{
"input": "065656",
"expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back"
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
},
{
"input": "0.1",
"expectedContent": "Amount 0.1 is invalid, please try again:\n1:retry\n9:Quit"
"input": "10000000",
"expectedContent": "Amount 10000000 is invalid, please try again:\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back"
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
},
{
"input": "0.001",
"expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
"input": "1.00",
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1222",
@@ -93,11 +93,11 @@
},
{
"input": "1",
"expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1234",
"expectedContent": "Your request has been sent. 065656 will receive 0.001 from {public_key}."
"expectedContent": "Your request has been sent. 065656 will receive {send_amount} from {session_id}."
}
]
},
@@ -106,7 +106,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "4",
@@ -119,7 +119,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "9",
@@ -132,7 +132,7 @@
"steps": [
{
"input": "",
"expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",

44
sample_tokens.json Normal file
View File

@@ -0,0 +1,44 @@
{
"ok": true,
"description": "Token holdings with current balances",
"result": {
"holdings": [
{
"contractAddress": "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
"tokenSymbol": "FSPTST",
"tokenDecimals": "6",
"balance": "8869964242"
},
{
"contractAddress": "0x724F2910D790B54A39a7638282a45B1D83564fFA",
"tokenSymbol": "GEO",
"tokenDecimals": "6",
"balance": "9884"
},
{
"contractAddress": "0x2105a206B7bec31E2F90acF7385cc8F7F5f9D273",
"tokenSymbol": "MFNK",
"tokenDecimals": "6",
"balance": "19788697"
},
{
"contractAddress": "0x63DE2Ac8D1008351Cc69Fb8aCb94Ba47728a7E83",
"tokenSymbol": "MILO",
"tokenDecimals": "6",
"balance": "75"
},
{
"contractAddress": "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
"tokenSymbol": "SOHAIL",
"tokenDecimals": "6",
"balance": "27874115"
},
{
"contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958",
"tokenSymbol": "SRF",
"tokenDecimals": "6",
"balance": "2745987"
}
]
}
}

View File

@@ -1,4 +1,4 @@
RELOAD verify_pin
RELOAD verify_create_pin
CATCH create_pin_mismatch flag_pin_mismatch 1
LOAD quit 0
HALT

View File

@@ -1,5 +1,6 @@
LOAD reset_transaction_amount 0
LOAD max_amount 10
RELOAD max_amount
MAP max_amount
MOUT back 0
HALT
@@ -10,5 +11,5 @@ CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0
LOAD get_recipient 12
LOAD get_sender 64
LOAD get_amount 12
LOAD get_amount 32
INCMP transaction_pin *

View File

@@ -1,4 +1,4 @@
LOAD save_pin 0
LOAD save_temporary_pin 6
HALT
LOAD verify_pin 8
LOAD verify_create_pin 8
INCMP account_creation *

View File

@@ -2,8 +2,8 @@ LOAD create_account 0
CATCH account_creation_failed flag_account_creation_failed 1
MOUT exit 0
HALT
LOAD save_pin 0
RELOAD save_pin
LOAD save_temporary_pin 6
RELOAD save_temporary_pin
CATCH . flag_incorrect_pin 1
INCMP quit 0
INCMP confirm_create_pin *

View File

@@ -1,9 +1,8 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD save_familyname 0
RELOAD save_familyname
MOUT back 0
HALT
LOAD save_familyname 0
RELOAD save_familyname
INCMP _ 0
INCMP pin_entry *

View File

@@ -1,8 +1,8 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD save_location 0
MOUT back 0
HALT
LOAD save_location 0
RELOAD save_location
INCMP _ 0
INCMP pin_entry *

View File

@@ -1,12 +1,8 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD save_firstname 0
RELOAD save_firstname
MOUT back 0
HALT
LOAD save_firstname 0
RELOAD save_firstname
INCMP _ 0
INCMP pin_entry *

View File

@@ -1,12 +1,14 @@
msgid "Your account balance is %s"
msgstr "Salio lako ni %s"
msgid "Your request has been sent. %s will receive %s from %s."
msgstr "Ombi lako limetumwa. %s atapokea %s kutoka kwa %s."
msgid "Your request has been sent. %s will receive %s %s from %s."
msgstr "Ombi lako limetumwa. %s atapokea %s %s kutoka kwa %s."
msgid "Thank you for using Sarafu. Goodbye!"
msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
msgid "For more help,please call: 0757628885"
msgstr "Kwa usaidizi zaidi,piga: 0757628885"
msgid "Balance: %s\n"
msgstr "Salio: %s\n"

View File

@@ -1 +1 @@
Balance: {{.check_balance}}
{{.check_balance}}

View File

@@ -1,5 +1,9 @@
LOAD set_default_voucher 8
RELOAD set_default_voucher
LOAD check_balance 64
RELOAD check_balance
LOAD check_vouchers 10
RELOAD check_vouchers
CATCH api_failure flag_api_call_error 1
MAP check_balance
MOUT send 1
@@ -9,7 +13,7 @@ MOUT help 4
MOUT quit 9
HALT
INCMP send 1
INCMP quit 2
INCMP my_vouchers 2
INCMP my_account 3
INCMP help 4
INCMP quit 9

View File

@@ -1 +1 @@
Salio: {{.check_balance}}
{{.check_balance}}

View File

@@ -0,0 +1 @@
My vouchers

View File

@@ -0,0 +1,8 @@
LOAD reset_account_authorized 16
RELOAD reset_account_authorized
MOUT select_voucher 1
MOUT voucher_details 2
MOUT back 0
HALT
INCMP _ 0
INCMP select_voucher 1

View File

@@ -0,0 +1 @@
You need a voucher to send

View File

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

View File

@@ -0,0 +1 @@
Unahitaji sarafu kutuma

View File

@@ -14,4 +14,6 @@ flag,flag_valid_pin,20,this is set when the given PIN is valid
flag,flag_allow_update,21,this is set to allow a user to update their profile data
flag,flag_single_edit,22,this is set to allow a user to edit a single profile item such as year of birth
flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid
flag,flag_api_call_error,25,this is set when communication to an external service fails
flag,flag_incorrect_voucher,24,this is set when the selected voucher is invalid
flag,flag_api_call_error,25,this is set when communication to an external service fails
flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher
1 flag flag_language_set 8 checks whether the user has set their prefered language
14 flag flag_allow_update 21 this is set to allow a user to update their profile data
15 flag flag_single_edit 22 this is set to allow a user to edit a single profile item such as year of birth
16 flag flag_incorrect_date_format 23 this is set when the given year of birth is invalid
17 flag flag_api_call_error flag_incorrect_voucher 25 24 this is set when communication to an external service fails this is set when the selected voucher is invalid
18 flag flag_api_call_error 25 this is set when communication to an external service fails
19 flag flag_no_active_voucher 26 this is set when a user does not have an active voucher

View File

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

View File

@@ -0,0 +1,15 @@
LOAD get_vouchers 0
MAP get_vouchers
MOUT back 0
MOUT quit 99
MNEXT next 11
MPREV prev 22
HALT
LOAD view_voucher 80
RELOAD view_voucher
CATCH . flag_incorrect_voucher 1
INCMP _ 0
INCMP quit 99
INCMP > 11
INCMP < 22
INCMP view_voucher *

View File

@@ -0,0 +1 @@
Select voucher

View File

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

View File

@@ -1,4 +1,5 @@
LOAD transaction_reset 0
CATCH no_voucher flag_no_active_voucher 1
MOUT back 0
HALT
LOAD validate_recipient 20

View File

@@ -0,0 +1,2 @@
Enter PIN to confirm selection:
{{.view_voucher}}

View File

@@ -0,0 +1,10 @@
MAP view_voucher
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 voucher_set *

View File

@@ -0,0 +1,2 @@
Weka PIN ili kuthibitisha chaguo:
{{.view_voucher}}

View File

@@ -0,0 +1 @@
Voucher details

View File

@@ -0,0 +1 @@
Success! {{.set_voucher}} is now your active voucher.

View File

@@ -0,0 +1,10 @@
LOAD reset_incorrect 6
CATCH incorrect_pin flag_incorrect_pin 1
CATCH _ flag_account_authorized 0
LOAD set_voucher 12
MAP set_voucher
MOUT back 0
MOUT quit 9
HALT
INCMP ^ 0
INCMP quit 9

View File

@@ -0,0 +1 @@
Hongera! {{.set_voucher}} ni Sarafu inayotumika sasa.