Merge pull request 'voucher-data' (#138) from voucher-data into master
Reviewed-on: #138
This commit is contained in:
		
						commit
						074345fcf9
					
				
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@ -12,6 +12,8 @@ require (
 | 
			
		||||
	gopkg.in/leonelquinteros/gotext.v1 v1.3.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							@ -18,6 +18,8 @@ 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/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=
 | 
			
		||||
 | 
			
		||||
@ -66,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) {
 | 
			
		||||
@ -75,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
 | 
			
		||||
}
 | 
			
		||||
@ -1051,13 +1056,13 @@ func (h *Handlers) SetDefaultVoucher(ctx context.Context, sym string, input []by
 | 
			
		||||
		if db.IsNotFound(err) {
 | 
			
		||||
			publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return res, 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, nil
 | 
			
		||||
				return res, err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Return if there is no voucher
 | 
			
		||||
@ -1114,54 +1119,33 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte)
 | 
			
		||||
		return res, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// process voucher data
 | 
			
		||||
	voucherSymbolList, voucherBalanceList := ProcessVouchers(vouchersResp.Result.Holdings)
 | 
			
		||||
	data := utils.ProcessVouchers(vouchersResp.Result.Holdings)
 | 
			
		||||
 | 
			
		||||
	prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
 | 
			
		||||
	err = prefixdb.Put(ctx, []byte("sym"), []byte(voucherSymbolList))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	// Store all voucher data
 | 
			
		||||
	dataMap := map[string]string{
 | 
			
		||||
		"sym":  data.Symbols,
 | 
			
		||||
		"bal":  data.Balances,
 | 
			
		||||
		"deci": data.Decimals,
 | 
			
		||||
		"addr": data.Addresses,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = prefixdb.Put(ctx, []byte("bal"), []byte(voucherBalanceList))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, nil
 | 
			
		||||
	for key, value := range dataMap {
 | 
			
		||||
		if err := h.prefixDb.Put(ctx, []byte(key), []byte(value)); err != 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
 | 
			
		||||
func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
 | 
			
		||||
	var res resource.Result
 | 
			
		||||
 | 
			
		||||
	// Read vouchers from the store
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
	prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
 | 
			
		||||
 | 
			
		||||
	voucherData, err := prefixdb.Get(ctx, []byte("sym"))
 | 
			
		||||
	voucherData, err := h.prefixDb.Get(ctx, []byte("sym"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, nil
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res.Content = string(voucherData)
 | 
			
		||||
@ -1172,8 +1156,6 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte)
 | 
			
		||||
// 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
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
@ -1182,126 +1164,51 @@ func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (r
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prefixdb := storage.NewSubPrefixDb(store, []byte("vouchers"))
 | 
			
		||||
 | 
			
		||||
	// Retrieve the voucher symbol list
 | 
			
		||||
	voucherSymbolList, err := prefixdb.Get(ctx, []byte("sym"))
 | 
			
		||||
	metadata, err := utils.GetVoucherData(ctx, h.prefixDb, inputStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, fmt.Errorf("failed to retrieve voucher symbol list: %v", err)
 | 
			
		||||
		return res, fmt.Errorf("failed to retrieve voucher data: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Retrieve the voucher balance list
 | 
			
		||||
	voucherBalanceList, err := prefixdb.Get(ctx, []byte("bal"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, fmt.Errorf("failed to retrieve voucher balance list: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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, utils.DATA_TEMPORARY_SYM, []byte(matchedSymbol))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, err
 | 
			
		||||
		}
 | 
			
		||||
		err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte(matchedBalance))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return res, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res.FlagReset = append(res.FlagReset, flag_incorrect_voucher)
 | 
			
		||||
		res.Content = fmt.Sprintf("%s\n%s", matchedSymbol, matchedBalance)
 | 
			
		||||
	} else {
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
// 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
 | 
			
		||||
	var err error
 | 
			
		||||
	store := h.userdataStore
 | 
			
		||||
 | 
			
		||||
	sessionId, ok := ctx.Value("SessionId").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return res, fmt.Errorf("missing session")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// get the current temporary symbol
 | 
			
		||||
	temporarySym, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
	// get the current temporary balance
 | 
			
		||||
	temporaryBal, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL)
 | 
			
		||||
	// Get temporary data
 | 
			
		||||
	tempData, err := utils.GetTemporaryVoucherData(ctx, h.userdataStore, sessionId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// set the active symbol
 | 
			
		||||
	err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_SYM, []byte(temporarySym))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
	// set the active balance
 | 
			
		||||
	err = store.WriteEntry(ctx, sessionId, utils.DATA_ACTIVE_BAL, []byte(temporaryBal))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
	// Set as active and clear temporary data
 | 
			
		||||
	if err := utils.UpdateVoucherData(ctx, h.userdataStore, sessionId, tempData); err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// reset the temporary symbol
 | 
			
		||||
	err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte(""))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
	// reset the temporary balance
 | 
			
		||||
	err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_BAL, []byte(""))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return res, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res.Content = string(temporarySym)
 | 
			
		||||
 | 
			
		||||
	res.Content = tempData.TokenSymbol
 | 
			
		||||
	return res, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,8 @@ import (
 | 
			
		||||
	"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 (
 | 
			
		||||
@ -1489,8 +1491,8 @@ func TestValidateAmount(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:    "Test with invalid amount format",
 | 
			
		||||
			input:   []byte("0.02ms"),
 | 
			
		||||
			name:      "Test with invalid amount format",
 | 
			
		||||
			input:     []byte("0.02ms"),
 | 
			
		||||
			activeBal: []byte("5"),
 | 
			
		||||
			expectedResult: resource.Result{
 | 
			
		||||
				FlagSet: []uint32{flag_invalid_amount},
 | 
			
		||||
@ -1819,40 +1821,6 @@ 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, utils.DATA_TEMPORARY_SYM).Return(temporarySym, nil)
 | 
			
		||||
	mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TEMPORARY_BAL).Return(temporaryBal, nil)
 | 
			
		||||
	mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_SYM, temporarySym).Return(nil)
 | 
			
		||||
	mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACTIVE_BAL, temporaryBal).Return(nil)
 | 
			
		||||
	mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_SYM, []byte("")).Return(nil)
 | 
			
		||||
	mockDataStore.On("WriteEntry", ctx, sessionId, utils.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) {
 | 
			
		||||
	fm, err := NewFlagManager(flagsPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@ -1934,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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,14 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
// VoucherHoldingResponse represents a single voucher holding
 | 
			
		||||
import dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | 
			
		||||
 | 
			
		||||
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"`
 | 
			
		||||
	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"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,14 @@ 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
 | 
			
		||||
@ -29,11 +37,7 @@ func (s *SubPrefixDb) toKey(k []byte) []byte {
 | 
			
		||||
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 {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								internal/testutil/mocks/subprefixdbmock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								internal/testutil/mocks/subprefixdbmock.go
									
									
									
									
									
										Normal 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)
 | 
			
		||||
}
 | 
			
		||||
@ -7,6 +7,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"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 {
 | 
			
		||||
@ -75,20 +76,8 @@ func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey
 | 
			
		||||
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) (*models.VoucherHoldingResponse, error) {
 | 
			
		||||
	return &models.VoucherHoldingResponse{
 | 
			
		||||
		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"`
 | 
			
		||||
			}{
 | 
			
		||||
		Result: models.VoucherResult{
 | 
			
		||||
			Holdings: []dataserviceapi.TokenHoldings{
 | 
			
		||||
				{
 | 
			
		||||
					ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
 | 
			
		||||
					TokenSymbol:     "SRF",
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,10 @@ const (
 | 
			
		||||
	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
									
								
							
							
						
						
									
										161
									
								
								internal/utils/vouchers.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										203
									
								
								internal/utils/vouchers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								internal/utils/vouchers_test.go
									
									
									
									
									
										Normal 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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user