Compare commits

..

No commits in common. "a48170321c03aa232aabf7065f98c08c947e6e9f" and "ff26ccc545d1b09456a1875d8e59ef290a15f349" have entirely different histories.

17 changed files with 437 additions and 979 deletions

View File

@ -29,10 +29,6 @@ const (
DATA_TEMPORARY_BAL DATA_TEMPORARY_BAL
DATA_ACTIVE_BAL DATA_ACTIVE_BAL
DATA_PUBLIC_KEY_REVERSE DATA_PUBLIC_KEY_REVERSE
DATA_TEMPORARY_DECIMAL
DATA_ACTIVE_DECIMAL
DATA_TEMPORARY_ADDRESS
DATA_ACTIVE_ADDRESS
) )
func typToBytes(typ DataTyp) []byte { func typToBytes(typ DataTyp) []byte {

View File

@ -1,67 +1,18 @@
package config package config
import ( import "git.grassecon.net/urdt/ussd/initializers"
"net/url"
"git.grassecon.net/urdt/ussd/initializers"
)
const (
createAccountPath = "/api/v2/account/create"
trackStatusPath = "/api/track"
balancePathPrefix = "/api/account"
trackPath = "/api/v2/account/status"
voucherHoldingsPathPrefix = "/api/v1/holdings"
voucherTransfersPathPrefix = "/api/v1/transfers/last10"
)
var (
custodialURLBase string
dataURLBase string
CustodialAPIKey string
DataAPIKey string
)
var ( var (
CreateAccountURL string CreateAccountURL string
TrackStatusURL string TrackStatusURL string
BalanceURL string BalanceURL string
TrackURL string TrackURL string
VoucherHoldingsURL string
VoucherTransfersURL string
) )
func setBase() error {
var err error
custodialURLBase = initializers.GetEnv("CUSTODIAL_URL_BASE", "http://localhost:5003")
dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
CustodialAPIKey = initializers.GetEnv("CUSTODIAL_API_KEY", "xd")
DataAPIKey = initializers.GetEnv("DATA_API_KEY", "xd")
_, err = url.JoinPath(custodialURLBase, "/foo")
if err != nil {
return err
}
_, err = url.JoinPath(dataURLBase, "/bar")
if err != nil {
return err
}
return nil
}
// LoadConfig initializes the configuration values after environment variables are loaded. // LoadConfig initializes the configuration values after environment variables are loaded.
func LoadConfig() error { func LoadConfig() {
err := setBase() CreateAccountURL = initializers.GetEnv("CREATE_ACCOUNT_URL", "http://localhost:5003/api/v2/account/create")
if err != nil { TrackStatusURL = initializers.GetEnv("TRACK_STATUS_URL", "https://custodial.sarafu.africa/api/track/")
return err BalanceURL = initializers.GetEnv("BALANCE_URL", "https://custodial.sarafu.africa/api/account/status/")
} TrackURL = initializers.GetEnv("TRACK_URL", "http://localhost:5003/api/v2/account/status")
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
TrackURL, _ = url.JoinPath(custodialURLBase, trackPath)
VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix)
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
return nil
} }

2
go.mod
View File

@ -12,8 +12,6 @@ require (
gopkg.in/leonelquinteros/gotext.v1 v1.3.1 gopkg.in/leonelquinteros/gotext.v1 v1.3.1
) )
require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect
require ( require (
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect

2
go.sum
View File

@ -18,8 +18,6 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU= 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/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 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=

View File

@ -66,7 +66,6 @@ type Handlers struct {
userdataStore common.DataStore userdataStore common.DataStore
flagManager *asm.FlagParser flagManager *asm.FlagParser
accountService remote.AccountServiceInterface accountService remote.AccountServiceInterface
prefixDb storage.PrefixDb
} }
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, accountService remote.AccountServiceInterface) (*Handlers, error) { func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, accountService remote.AccountServiceInterface) (*Handlers, error) {
@ -76,14 +75,10 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, accountService r
userDb := &common.UserDataStore{ userDb := &common.UserDataStore{
Db: userdataStore, Db: userdataStore,
} }
// Instantiate the SubPrefixDb with "vouchers" prefix
prefixDb := storage.NewSubPrefixDb(userdataStore, []byte("vouchers"))
h := &Handlers{ h := &Handlers{
userdataStore: userDb, userdataStore: userDb,
flagManager: appFlags, flagManager: appFlags,
accountService: accountService, accountService: accountService,
prefixDb: prefixDb,
} }
return h, nil return h, nil
} }
@ -149,12 +144,12 @@ 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 { func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created") flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
r, err := h.accountService.CreateAccount(ctx) okResponse, err := h.accountService.CreateAccount(ctx)
if err != nil { if err != nil {
return err return err
} }
trackingId := r.TrackingId trackingId := okResponse.Result["trackingId"].(string)
publicKey := r.PublicKey publicKey := okResponse.Result["publicKey"].(string)
data := map[common.DataTyp]string{ data := map[common.DataTyp]string{
common.DATA_TRACKING_ID: trackingId, common.DATA_TRACKING_ID: trackingId,
@ -682,7 +677,6 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
@ -700,13 +694,14 @@ func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input
balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey)) balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey))
if err != nil { if err != nil {
return res, nil
}
if !balanceResponse.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error) res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil return res, nil
} }
res.FlagReset = append(res.FlagReset, flag_api_error) res.FlagReset = append(res.FlagReset, flag_api_error)
balance := balanceResponse.Result.Balance
//balance := balanceResponse.Result.Balance
balance := balanceResponse.Balance
switch balanceType { switch balanceType {
case "my": case "my":
@ -1056,23 +1051,23 @@ func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []by
if db.IsNotFound(err) { if db.IsNotFound(err) {
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
if err != nil { if err != nil {
return res, err return res, nil
} }
// Fetch vouchers from the API using the public key // Fetch vouchers from the API using the public key
vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey)) vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil { if err != nil {
return res, err return res, nil
} }
// Return if there is no voucher // Return if there is no voucher
if len(vouchersResp) == 0 { if len(vouchersResp.Result.Holdings) == 0 {
res.FlagSet = append(res.FlagSet, flag_no_active_voucher) res.FlagSet = append(res.FlagSet, flag_no_active_voucher)
return res, nil return res, nil
} }
// Use only the first voucher // Use only the first voucher
firstVoucher := vouchersResp[0] firstVoucher := vouchersResp.Result.Holdings[0]
defaultSym := firstVoucher.TokenSymbol defaultSym := firstVoucher.TokenSymbol
defaultBal := firstVoucher.Balance defaultBal := firstVoucher.Balance
@ -1119,33 +1114,54 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
data := utils.ProcessVouchers(vouchersResp) // process voucher data
voucherSymbolList, voucherBalanceList := ProcessVouchers(vouchersResp.Result.Holdings)
// Store all voucher data prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
dataMap := map[string]string{ err = prefixdb.Put(ctx, []byte("sym"), []byte(voucherSymbolList))
"sym": data.Symbols, if err != nil {
"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
} }
err = prefixdb.Put(ctx, []byte("bal"), []byte(voucherBalanceList))
if err != nil {
return res, nil
} }
return res, nil return res, nil
} }
// ProcessVouchers formats the holdings into symbol and balance lists.
func ProcessVouchers(holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}) (string, string) {
var numberedSymbols, numberedBalances []string
for i, voucher := range holdings {
numberedSymbols = append(numberedSymbols, fmt.Sprintf("%d:%s", i+1, voucher.TokenSymbol))
numberedBalances = append(numberedBalances, fmt.Sprintf("%d:%s", i+1, voucher.Balance))
}
voucherSymbolList := strings.Join(numberedSymbols, "\n")
voucherBalanceList := strings.Join(numberedBalances, "\n")
return voucherSymbolList, voucherBalanceList
}
// GetVoucherList fetches the list of vouchers and formats them // GetVoucherList fetches the list of vouchers and formats them
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
// Read vouchers from the store // Read vouchers from the store
voucherData, err := h.prefixDb.Get(ctx, []byte("sym")) store := h.userdataStore
prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
voucherData, err := prefixdb.Get(ctx, []byte("sym"))
if err != nil { if err != nil {
return res, err return res, nil
} }
res.Content = string(voucherData) res.Content = string(voucherData)
@ -1156,6 +1172,8 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte)
// ViewVoucher retrieves the token holding and balance from the subprefixDB // ViewVoucher retrieves the token holding and balance from the subprefixDB
func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
@ -1164,52 +1182,126 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r
flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher") flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher")
inputStr := string(input) inputStr := string(input)
if inputStr == "0" || inputStr == "99" { if inputStr == "0" || inputStr == "99" {
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher) res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
return res, nil return res, nil
} }
metadata, err := utils.GetVoucherData(ctx, h.prefixDb, inputStr) prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
// Retrieve the voucher symbol list
voucherSymbolList, err := prefixdb.Get(ctx, []byte("sym"))
if err != nil { if err != nil {
return res, fmt.Errorf("failed to retrieve voucher data: %v", err) return res, fmt.Errorf("failed to retrieve voucher symbol list: %v", err)
} }
if metadata == nil { // Retrieve the voucher balance list
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher) voucherBalanceList, err := prefixdb.Get(ctx, []byte("bal"))
return res, nil if err != nil {
return res, fmt.Errorf("failed to retrieve voucher balance list: %v", err)
} }
if err := utils.StoreTemporaryVoucher(ctx, h.userdataStore, sessionId, metadata); err != nil { // match the voucher symbol and balance with the input
matchedSymbol, matchedBalance := MatchVoucher(inputStr, string(voucherSymbolList), string(voucherBalanceList))
// If a match is found, write the temporary sym and balance
if matchedSymbol != "" && matchedBalance != "" {
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_SYM, []byte(matchedSymbol))
if err != nil {
return res, err
}
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_BAL, []byte(matchedBalance))
if err != nil {
return res, err return res, err
} }
res.FlagReset = append(res.FlagReset, flag_incorrect_voucher) res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
res.Content = fmt.Sprintf("%s\n%s", metadata.TokenSymbol, metadata.Balance) res.Content = fmt.Sprintf("%s\n%s", matchedSymbol, matchedBalance)
} else {
res.FlagSet = append(res.FlagSet, flag_incorrect_voucher)
}
return res, nil return res, nil
} }
// SetVoucher retrieves the temp voucher data and sets it as the active data // MatchVoucher finds the matching voucher symbol and balance based on the input.
func MatchVoucher(inputStr string, voucherSymbols, voucherBalances string) (string, string) {
// Split the lists into slices for processing
symbols := strings.Split(voucherSymbols, "\n")
balances := strings.Split(voucherBalances, "\n")
var matchedSymbol, matchedBalance string
for i, symbol := range symbols {
symbolParts := strings.SplitN(symbol, ":", 2)
if len(symbolParts) != 2 {
continue
}
voucherNum := symbolParts[0]
voucherSymbol := symbolParts[1]
// Check if input matches either the number or the symbol
if inputStr == voucherNum || strings.EqualFold(inputStr, voucherSymbol) {
matchedSymbol = voucherSymbol
// Ensure there's a corresponding balance
if i < len(balances) {
matchedBalance = strings.SplitN(balances[i], ":", 2)[1]
}
break
}
}
return matchedSymbol, matchedBalance
}
// SetVoucher retrieves the temporary sym and balance,
// sets them as the active data and
// clears the temporary data
func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SetVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
// get the current temporary symbol
// Get temporary data temporarySym, err := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_SYM)
tempData, err := utils.GetTemporaryVoucherData(ctx, h.userdataStore, sessionId) if err != nil {
return res, err
}
// get the current temporary balance
temporaryBal, err := store.ReadEntry(ctx, sessionId, common.DATA_TEMPORARY_BAL)
if err != nil { if err != nil {
return res, err return res, err
} }
// Set as active and clear temporary data // set the active symbol
if err := utils.UpdateVoucherData(ctx, h.userdataStore, sessionId, tempData); err != nil { err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_SYM, []byte(temporarySym))
if err != nil {
return res, err
}
// set the active balance
err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_BAL, []byte(temporaryBal))
if err != nil {
return res, err return res, err
} }
res.Content = tempData.TokenSymbol // reset the temporary symbol
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_SYM, []byte(""))
if err != nil {
return res, err
}
// reset the temporary balance
err = store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_BAL, []byte(""))
if err != nil {
return res, err
}
res.Content = string(temporarySym)
return res, nil return res, nil
} }

View File

@ -24,8 +24,6 @@ import (
"github.com/grassrootseconomics/eth-custodial/pkg/api" "github.com/grassrootseconomics/eth-custodial/pkg/api"
testdataloader "github.com/peteole/testdata-loader" testdataloader "github.com/peteole/testdata-loader"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
var ( var (
@ -88,14 +86,18 @@ func TestCreateAccount(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
serverResponse *models.AccountResult serverResponse *api.OKResponse
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test account creation success", name: "Test account creation success",
serverResponse: &models.AccountResult{ serverResponse: &api.OKResponse{
TrackingId: "1234567890", Ok: true,
PublicKey: "0xD3adB33f", Description: "Account creation successed",
Result: map[string]any{
"trackingId": "1234567890",
"publicKey": "0xD3adB33f",
},
}, },
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_account_created}, FlagSet: []uint32{flag_account_created},
@ -115,9 +117,9 @@ func TestCreateAccount(t *testing.T) {
flagManager: fm.parser, flagManager: fm.parser,
} }
publicKey := tt.serverResponse.PublicKey publicKey := tt.serverResponse.Result["publicKey"].(string)
data := map[common.DataTyp]string{ data := map[common.DataTyp]string{
common.DATA_TRACKING_ID: tt.serverResponse.TrackingId, common.DATA_TRACKING_ID: tt.serverResponse.Result["trackingId"].(string),
common.DATA_PUBLIC_KEY: publicKey, common.DATA_PUBLIC_KEY: publicKey,
} }
@ -1051,7 +1053,7 @@ func TestCheckAccountStatus(t *testing.T) {
name string name string
input []byte input []byte
serverResponse *api.OKResponse serverResponse *api.OKResponse
response *models.TrackStatusResult response *models.TrackStatusResponse
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
@ -1064,13 +1066,26 @@ func TestCheckAccountStatus(t *testing.T) {
"active": true, "active": true,
}, },
}, },
response: &models.TrackStatusResult { response: &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(), CreatedAt: time.Now(),
Status: "SUCCESS", Status: "SUCCESS",
TransferValue: json.Number("0.5"), TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def", TxHash: "0x123abc456def",
TxType: "transfer", TxType: "transfer",
}, },
},
},
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_account_success}, FlagSet: []uint32{flag_account_success},
FlagReset: []uint32{flag_api_error, flag_account_pending}, FlagReset: []uint32{flag_api_error, flag_account_pending},
@ -1079,13 +1094,26 @@ func TestCheckAccountStatus(t *testing.T) {
{ {
name: "Test when the account is not yet on the sarafu network", name: "Test when the account is not yet on the sarafu network",
input: []byte("TrackingId1234"), input: []byte("TrackingId1234"),
response: &models.TrackStatusResult{ response: &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(), CreatedAt: time.Now(),
Status: "SUCCESS", Status: "SUCCESS",
TransferValue: json.Number("0.5"), TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def", TxHash: "0x123abc456def",
TxType: "transfer", TxType: "transfer",
}, },
},
},
serverResponse: &api.OKResponse{ serverResponse: &api.OKResponse{
Ok: true, Ok: true,
Description: "Account creation succeeded", Description: "Account creation succeeded",
@ -1110,7 +1138,7 @@ func TestCheckAccountStatus(t *testing.T) {
flagManager: fm.parser, flagManager: fm.parser,
} }
status := tt.response.Status status := tt.response.Result.Transaction.Status
// Define expected interactions with the mock // Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_PUBLIC_KEY).Return(tt.input, nil) mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_PUBLIC_KEY).Return(tt.input, nil)
@ -1129,6 +1157,7 @@ func TestCheckAccountStatus(t *testing.T) {
// Assert that expectations were met // Assert that expectations were met
mockDataStore.AssertExpectations(t) mockDataStore.AssertExpectations(t)
}) })
} }
@ -1789,6 +1818,40 @@ func TestConfirmPin(t *testing.T) {
} }
} }
func TestSetVoucher(t *testing.T) {
mockDataStore := new(mocks.MockUserDataStore)
sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId)
temporarySym := []byte("tempSym")
temporaryBal := []byte("tempBal")
// Set expectations for the mock data store
mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_TEMPORARY_SYM).Return(temporarySym, nil)
mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_TEMPORARY_BAL).Return(temporaryBal, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, common.DATA_ACTIVE_SYM, temporarySym).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, common.DATA_ACTIVE_BAL, temporaryBal).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, common.DATA_TEMPORARY_SYM, []byte("")).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, common.DATA_TEMPORARY_BAL, []byte("")).Return(nil)
h := &Handlers{
userdataStore: mockDataStore,
}
// Call the method under test
res, err := h.SetVoucher(ctx, "someSym", []byte{})
// Assert that no errors occurred
assert.NoError(t, err)
// Assert that the result content is correct
assert.Equal(t, string(temporarySym), res.Content)
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
}
func TestFetchCustodialBalances(t *testing.T) { func TestFetchCustodialBalances(t *testing.T) {
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
if err != nil { if err != nil {
@ -1803,15 +1866,37 @@ func TestFetchCustodialBalances(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
balanceResponse *models.BalanceResult balanceResonse *models.BalanceResponse
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test when fetch custodial balances is not a success", name: "Test when fetch custodial balances is not a success",
balanceResponse: &models.BalanceResult{ balanceResonse: &models.BalanceResponse{
Ok: false,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO", Balance: "0.003 CELO",
Nonce: json.Number("0"), Nonce: json.Number("0"),
}, },
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_api_error},
},
},
{
name: "Test when fetch custodial balances 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{ expectedResult: resource.Result{
FlagReset: []uint32{flag_api_error}, FlagReset: []uint32{flag_api_error},
}, },
@ -1834,7 +1919,7 @@ func TestFetchCustodialBalances(t *testing.T) {
// Set up the expected behavior of the mock // Set up the expected behavior of the mock
mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResponse, nil) mockCreateAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResonse, nil)
// Call the method // Call the method
res, _ := h.FetchCustodialBalances(ctx, "fetch_custodial_balances", []byte("")) res, _ := h.FetchCustodialBalances(ctx, "fetch_custodial_balances", []byte(""))
@ -1848,244 +1933,3 @@ 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 []dataserviceapi.TokenHoldings
expectedResult resource.Result
}{
{
name: "Test set default voucher when no active voucher exists",
vouchersResp: []dataserviceapi.TokenHoldings {
dataserviceapi.TokenHoldings{
ContractAddress: "0x123",
TokenSymbol: "TOKEN1",
TokenDecimals: "18",
Balance: "100",
},
},
expectedResult: resource.Result{},
},
{
name: "Test no vouchers available",
vouchersResp: []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, common.DATA_ACTIVE_SYM).Return([]byte(""), notFoundErr)
mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockAccountService.On("FetchVouchers", string(publicKey)).Return(tt.vouchersResp, nil)
if len(tt.vouchersResp) > 0 {
firstVoucher := tt.vouchersResp[0]
mockDataStore.On("WriteEntry", ctx, sessionId, common.DATA_ACTIVE_SYM, []byte(firstVoucher.TokenSymbol)).Return(nil)
mockDataStore.On("WriteEntry", ctx, sessionId, common.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, common.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil)
mockVouchersResponse := []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[common.DataTyp]string{
common.DATA_TEMPORARY_SYM: "SRF",
common.DATA_TEMPORARY_BAL: "100",
common.DATA_TEMPORARY_DECIMAL: "6",
common.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[common.DataTyp][]byte{
common.DATA_ACTIVE_SYM: []byte(tempData.TokenSymbol),
common.DATA_ACTIVE_BAL: []byte(tempData.Balance),
common.DATA_ACTIVE_DECIMAL: []byte(tempData.TokenDecimals),
common.DATA_ACTIVE_ADDRESS: []byte(tempData.ContractAddress),
}
// Define the temporary entries to be cleared
tempEntries := map[common.DataTyp][]byte{
common.DATA_TEMPORARY_SYM: []byte(""),
common.DATA_TEMPORARY_BAL: []byte(""),
common.DATA_TEMPORARY_DECIMAL: []byte(""),
common.DATA_TEMPORARY_ADDRESS: []byte(""),
}
// Mocking ReadEntry calls for temporary data retrieval
mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_TEMPORARY_SYM).Return([]byte(tempData.TokenSymbol), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_TEMPORARY_BAL).Return([]byte(tempData.Balance), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, common.DATA_TEMPORARY_DECIMAL).Return([]byte(tempData.TokenDecimals), nil)
mockDataStore.On("ReadEntry", ctx, sessionId, common.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

@ -1,6 +1,10 @@
package models package models
type AccountResult struct { type AccountResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"` // Include the description field
Result struct {
PublicKey string `json:"publicKey"` PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"` TrackingId string `json:"trackingId"`
} `json:"result"`
} }

View File

@ -3,15 +3,10 @@ package models
import "encoding/json" import "encoding/json"
//type BalanceResponse struct { type BalanceResponse struct {
// Ok bool `json:"ok"` Ok bool `json:"ok"`
// Result struct { Result struct {
// Balance string `json:"balance"`
// Nonce json.Number `json:"nonce"`
// } `json:"result"`
//}
//
type BalanceResult struct {
Balance string `json:"balance"` Balance string `json:"balance"`
Nonce json.Number `json:"nonce"` Nonce json.Number `json:"nonce"`
} `json:"result"`
} }

View File

@ -4,18 +4,23 @@ import (
"encoding/json" "encoding/json"
"time" "time"
) )
//type Transaction struct { type Transaction struct {
// CreatedAt time.Time `json:"createdAt"`
// Status string `json:"status"`
// TransferValue json.Number `json:"transferValue"`
// TxHash string `json:"txHash"`
// TxType string `json:"txType"`
//}
type TrackStatusResult struct {
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
Status string `json:"status"` Status string `json:"status"`
TransferValue json.Number `json:"transferValue"` TransferValue json.Number `json:"transferValue"`
TxHash string `json:"txHash"` TxHash string `json:"txHash"`
TxType string `json:"txType"` TxType string `json:"txType"`
} }
type TrackStatusResponse struct {
Ok bool `json:"ok"`
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"`
}
} `json:"result"`
}

View File

@ -1 +1,15 @@
package models package models
// VoucherHoldingResponse represents a single voucher holding
type VoucherHoldingResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result struct {
Holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
} `json:"holdings"`
} `json:"result"`
}

View File

@ -10,14 +10,6 @@ const (
DATATYPE_USERSUB = 64 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 { type SubPrefixDb struct {
store db.Db store db.Db
pfx []byte pfx []byte
@ -37,7 +29,11 @@ func (s *SubPrefixDb) toKey(k []byte) []byte {
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(DATATYPE_USERSUB) s.store.SetPrefix(DATATYPE_USERSUB)
key = s.toKey(key) key = s.toKey(key)
return s.store.Get(ctx, key) v, err := s.store.Get(ctx, key)
if err != nil {
return nil, err
}
return v, nil
} }
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {

View File

@ -5,7 +5,6 @@ import (
"git.grassecon.net/urdt/ussd/internal/models" "git.grassecon.net/urdt/ussd/internal/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api" "github.com/grassrootseconomics/eth-custodial/pkg/api"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
@ -14,28 +13,28 @@ type MockAccountService struct {
mock.Mock mock.Mock
} }
func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { func (m *MockAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
args := m.Called() args := m.Called()
return args.Get(0).(*models.AccountResult), args.Error(1)
}
func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
args := m.Called(publicKey)
return args.Get(0).(*models.BalanceResult), args.Error(1)
}
func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId string) (*api.OKResponse, error) {
args := m.Called(trackingId)
return args.Get(0).(*api.OKResponse), args.Error(1) return args.Get(0).(*api.OKResponse), args.Error(1)
} }
func (m *MockAccountService) CheckAccountStatus(ctx context.Context,publicKey string) (*models.TrackStatusResult, error) { func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) {
args := m.Called(publicKey) args := m.Called(publicKey)
return args.Get(0).(*models.TrackStatusResult), args.Error(1) 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) ([]dataserviceapi.TokenHoldings, error) { func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
args := m.Called(publicKey) args := m.Called(publicKey)
return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1) return args.Get(0).(*models.VoucherHoldingResponse), args.Error(1)
} }

View File

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

@ -7,35 +7,59 @@ import (
"git.grassecon.net/urdt/ussd/internal/models" "git.grassecon.net/urdt/ussd/internal/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api" "github.com/grassrootseconomics/eth-custodial/pkg/api"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
type TestAccountService struct { type TestAccountService struct {
} }
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { func (tas *TestAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
return &models.AccountResult { return &api.OKResponse{
TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d", Ok: true,
PublicKey: "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD", Description: "Account creation succeeded",
Result: map[string]any{
"trackingId": "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d",
"publicKey": "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD",
},
}, nil }, nil
} }
func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) {
balanceResponse := &models.BalanceResult { balanceResponse := &models.BalanceResponse{
Ok: true,
Result: struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`
}{
Balance: "0.003 CELO", Balance: "0.003 CELO",
Nonce: json.Number("0"), Nonce: json.Number("0"),
},
} }
return balanceResponse, nil return balanceResponse, nil
} }
func (tas *TestAccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) { func (tas *TestAccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) {
return &models.TrackStatusResult { 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(), CreatedAt: time.Now(),
Status: "SUCCESS", Status: "SUCCESS",
TransferValue: json.Number("0.5"), TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def", TxHash: "0x123abc456def",
TxType: "transfer", TxType: "transfer",
}, nil },
},
}
return trackResponse, nil
} }
func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) { func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) {
@ -48,13 +72,30 @@ func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey
}, nil }, nil
} }
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
return []dataserviceapi.TokenHoldings { return &models.VoucherHoldingResponse{
dataserviceapi.TokenHoldings { Ok: true,
Result: struct {
Holdings []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
} `json:"holdings"`
}{
Holdings: []struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}{
{
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF", TokenSymbol: "SRF",
TokenDecimals: "6", TokenDecimals: "6",
Balance: "2745987", Balance: "2745987",
}, },
},
},
}, nil }, nil
} }

View File

@ -1,180 +0,0 @@
package utils
import (
"context"
"fmt"
"strings"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/common"
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 len(parts) != 2 {
continue
}
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 common.DataStore.
func StoreTemporaryVoucher(ctx context.Context, store common.DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
entries := map[common.DataTyp][]byte{
common.DATA_TEMPORARY_SYM: []byte(data.TokenSymbol),
common.DATA_TEMPORARY_BAL: []byte(data.Balance),
common.DATA_TEMPORARY_DECIMAL: []byte(data.TokenDecimals),
common.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 common.DataStore.
func GetTemporaryVoucherData(ctx context.Context, store common.DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
keys := []common.DataTyp{
common.DATA_TEMPORARY_SYM,
common.DATA_TEMPORARY_BAL,
common.DATA_TEMPORARY_DECIMAL,
common.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 and clears the temporary voucher data in the common.DataStore.
func UpdateVoucherData(ctx context.Context, store common.DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
// Active voucher data entries
activeEntries := map[common.DataTyp][]byte{
common.DATA_ACTIVE_SYM: []byte(data.TokenSymbol),
common.DATA_ACTIVE_BAL: []byte(data.Balance),
common.DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals),
common.DATA_ACTIVE_ADDRESS: []byte(data.ContractAddress),
}
// Clear temporary voucher data entries
tempEntries := map[common.DataTyp][]byte{
common.DATA_TEMPORARY_SYM: []byte(""),
common.DATA_TEMPORARY_BAL: []byte(""),
common.DATA_TEMPORARY_DECIMAL: []byte(""),
common.DATA_TEMPORARY_ADDRESS: []byte(""),
}
// Write active data
for key, value := range activeEntries {
if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
return err
}
}
// Clear temporary data
for key, value := range tempEntries {
if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
return err
}
}
return nil
}

View File

@ -1,223 +0,0 @@
package utils
import (
"context"
"testing"
"github.com/alecthomas/assert/v2"
"github.com/stretchr/testify/require"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/common"
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, *common.UserDataStore) {
ctx := context.Background()
// Initialize memDb
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
require.NoError(t, err, "Failed to connect to memDb")
// Create common.UserDataStore with memDb
store := &common.UserDataStore{Db: db}
t.Cleanup(func() {
db.Close() // Ensure the DB is closed after each test
})
return ctx, store
}
// AssertEmptyValue checks if a value is empty/nil/zero
func AssertEmptyValue(t *testing.T, value []byte, msgAndArgs ...interface{}) {
assert.Equal(t, len(value), 0, msgAndArgs...)
}
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[common.DataTyp][]byte{
common.DATA_TEMPORARY_SYM: []byte("SRF"),
common.DATA_TEMPORARY_BAL: []byte("200"),
common.DATA_TEMPORARY_DECIMAL: []byte("6"),
common.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[common.DataTyp][]byte{
common.DATA_ACTIVE_SYM: []byte(newData.TokenSymbol),
common.DATA_ACTIVE_BAL: []byte(newData.Balance),
common.DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals),
common.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)
}
// Verify temporary data was cleared
tempKeys := []common.DataTyp{
common.DATA_TEMPORARY_SYM,
common.DATA_TEMPORARY_BAL,
common.DATA_TEMPORARY_DECIMAL,
common.DATA_TEMPORARY_ADDRESS,
}
for _, key := range tempKeys {
storedValue, err := store.ReadEntry(ctx, sessionId, key)
require.NoError(t, err)
AssertEmptyValue(t, storedValue, "Temporary data not cleared for key %v", key)
}
}

View File

@ -4,29 +4,27 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "os"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
"git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/internal/models" "git.grassecon.net/urdt/ussd/internal/models"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
) )
var ( var (
okResponse api.OKResponse
errResponse api.ErrResponse
) )
type AccountServiceInterface interface { type AccountServiceInterface interface {
//CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error)
CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) CreateAccount(ctx context.Context) (*api.OKResponse, error)
CreateAccount(ctx context.Context) (*models.AccountResult, error) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error)
//CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error)
// TODO: poorly named method seems to be checking transaction
CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error)
TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error)
FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error)
//FetchVouchers(ctx context.Context, publicKey string) (*api.OKResponse, error)
} }
type AccountService struct { type AccountService struct {
@ -41,38 +39,32 @@ type AccountService struct {
// - 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. // - 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. // - 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 // If no error occurs, this will be nil
func (as *AccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) { func (as *AccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) {
var r models.TrackStatusResult resp, err := http.Get(config.BalanceURL + trackingId)
if err != nil {
return nil, err
}
defer resp.Body.Close()
ep, err := url.JoinPath(config.TrackURL, trackingId) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req, err := http.NewRequest("GET", ep, nil) var trackResp models.TrackStatusResponse
err = json.Unmarshal(body, &trackResp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &trackResp, nil
_, err = doCustodialRequest(ctx, req, &r)
if err != nil {
return nil, err
}
return &r, nil
} }
func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) { func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) {
var okResponse api.OKResponse
var errResponse api.ErrResponse
var err error var err error
// Construct the URL with the path parameter
ep, err := url.JoinPath(config.TrackURL, publicKey) url := fmt.Sprintf("%s/%s", config.TrackURL, publicKey)
if err != nil { req, err := http.NewRequest("GET", url, nil)
return nil, err
}
req, err := http.NewRequest("GET", ep, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -112,83 +104,41 @@ func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey stri
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint. // CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
// Parameters: // Parameters:
// - publicKey: The public key associated with the account whose balance needs to be checked. // - publicKey: The public key associated with the account whose balance needs to be checked.
func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) {
var balanceResult models.BalanceResult resp, err := http.Get(config.BalanceURL + publicKey)
ep, err := url.JoinPath(config.BalanceURL, publicKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close()
req, err := http.NewRequest("GET", ep, nil) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var balanceResp models.BalanceResponse
_, err = doCustodialRequest(ctx, req, &balanceResult) err = json.Unmarshal(body, &balanceResp)
return &balanceResult, err if err != nil {
return nil, err
}
return &balanceResp, nil
} }
// CreateAccount creates a new account in the custodial system. // CreateAccount creates a new account in the custodial system.
// Returns: // Returns:
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account. // - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
// If there is an error during the request or processing, this will be nil. // 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. // - 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. // If no error occurs, this will be nil.
func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { func (as *AccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) {
var r models.AccountResult var err error
// Create a new request // Create a new request
req, err := http.NewRequest("POST", config.CreateAccountURL, nil) req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, err = doCustodialRequest(ctx, req, &r)
if err != nil {
return nil, err
}
return &r, nil
}
// 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) ([]dataserviceapi.TokenHoldings, error) {
var r []dataserviceapi.TokenHoldings
req, err := http.NewRequest("GET", config.VoucherHoldingsURL, nil)
if err != nil {
return nil, err
}
_, err = doDataRequest(ctx, req, r)
if err != nil {
return nil, err
}
return r, nil
//
// file, err := os.Open("sample_tokens.json")
// if err != nil {
// return nil, err
// }
// defer file.Close()
// var holdings models.VoucherHoldingResponse
//
// if err := json.NewDecoder(file).Decode(&holdings); err != nil {
// return nil, err
// }
// return &holdings, nil
}
func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
var okResponse api.OKResponse
var errResponse api.ErrResponse
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GE-KEY", "xd")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
errResponse.Description = err.Error() errResponse.Description = err.Error()
@ -215,22 +165,21 @@ func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKRespons
return nil, errors.New("Empty api result") return nil, errors.New("Empty api result")
} }
return &okResponse, nil return &okResponse, nil
}
v, err := json.Marshal(okResponse.Result) // 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 { if err != nil {
return nil, err return nil, err
} }
defer file.Close()
var holdings models.VoucherHoldingResponse
err = json.Unmarshal(v, &rcpt) if err := json.NewDecoder(file).Decode(&holdings); err != nil {
return &okResponse, err return nil, err
} }
return &holdings, nil
func doCustodialRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
req.Header.Set("X-GE-KEY", config.CustodialAPIKey)
return doRequest(ctx, req, rcpt)
}
func doDataRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
req.Header.Set("X-GE-KEY", config.DataAPIKey)
return doRequest(ctx, req, rcpt)
} }