diff --git a/.env.example b/.env.example index 97d3317..ab370a7 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,7 @@ DB_SSLMODE=disable DB_TIMEZONE=Africa/Nairobi #External API Calls -CREATE_ACCOUNT_URL=https://custodial.sarafu.africa/api/account/create +CREATE_ACCOUNT_URL=http://localhost:5003/api/v2/account/create TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/ BALANCE_URL=https://custodial.sarafu.africa/api/account/status/ +TRACK_URL=http://localhost:5003/api/v2/account/status diff --git a/config/config.go b/config/config.go index dba0da7..43466c3 100644 --- a/config/config.go +++ b/config/config.go @@ -6,11 +6,13 @@ var ( CreateAccountURL string TrackStatusURL string BalanceURL string + TrackURL string ) // LoadConfig initializes the configuration values after environment variables are loaded. func LoadConfig() { - CreateAccountURL = initializers.GetEnv("CREATE_ACCOUNT_URL", "https://custodial.sarafu.africa/api/account/create") + CreateAccountURL = initializers.GetEnv("CREATE_ACCOUNT_URL", "http://localhost:5003/api/v2/account/create") TrackStatusURL = initializers.GetEnv("TRACK_STATUS_URL", "https://custodial.sarafu.africa/api/track/") BalanceURL = initializers.GetEnv("BALANCE_URL", "https://custodial.sarafu.africa/api/account/status/") + TrackURL = initializers.GetEnv("TRACK_URL", "http://localhost:5003/api/v2/account/status") } diff --git a/go.mod b/go.mod index 38be305..7112503 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,48 @@ module git.grassecon.net/urdt/ussd -go 1.22.6 +go 1.23.0 + +toolchain go1.23.2 require ( git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b github.com/alecthomas/assert/v2 v2.2.2 github.com/peteole/testdata-loader v0.3.0 gopkg.in/leonelquinteros/gotext.v1 v1.3.1 + ) -require github.com/joho/godotenv v1.5.1 // indirect +require github.com/joho/godotenv v1.5.1 + +require ( + github.com/grassrootseconomics/eth-custodial v1.3.0-beta + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.18.0 // indirect +) require ( github.com/alecthomas/participle/v2 v2.0.0 // indirect github.com/alecthomas/repr v0.2.0 // indirect github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.9.0 github.com/x448/float16 v0.8.4 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + ) + + diff --git a/go.sum b/go.sum index 2abec41..d566e2c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,3 @@ -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y= -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= -git.defalsify.org/vise.git v0.2.0 h1:X2ZgiGRq4C+9qOlDMP0b/oE5QHjVQNT4aEFZB88ST0Q= -git.defalsify.org/vise.git v0.2.0/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU= git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= @@ -12,33 +8,65 @@ github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE= github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU= +github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= +github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s= +github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I= github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/handlers/server/accountservice.go b/internal/handlers/server/accountservice.go index ebe825e..58774ce 100644 --- a/internal/handlers/server/accountservice.go +++ b/internal/handlers/server/accountservice.go @@ -1,7 +1,10 @@ package server import ( + "context" "encoding/json" + "errors" + "fmt" "io" "net/http" "os" @@ -9,12 +12,19 @@ import ( "git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/internal/models" + "github.com/grassrootseconomics/eth-custodial/pkg/api" +) + +var ( + okResponse api.OKResponse + errResponse api.ErrResponse ) type AccountServiceInterface interface { - CheckBalance(publicKey string) (*models.BalanceResponse, error) - CreateAccount() (*models.AccountResponse, error) - CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) + CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) + CreateAccount(ctx context.Context) (*api.OKResponse, error) + CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) + TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) FetchVouchers(publicKey string) (*models.VoucherHoldingResponse, error) } @@ -24,8 +34,6 @@ type AccountService struct { type TestAccountService struct { } -// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID. -// // Parameters: // - trackingId: A unique identifier for the account.This should be obtained from a previous call to // CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the @@ -34,9 +42,9 @@ type TestAccountService struct { // Returns: // - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string. // - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. -// If no error occurs, this will be nil. -func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { - resp, err := http.Get(config.TrackStatusURL + trackingId) +// If no error occurs, this will be nil +func (as *AccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) { + resp, err := http.Get(config.BalanceURL + trackingId) if err != nil { return nil, err } @@ -46,18 +54,61 @@ func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackSt if err != nil { return nil, err } + var trackResp models.TrackStatusResponse err = json.Unmarshal(body, &trackResp) if err != nil { return nil, err } return &trackResp, nil + +} + +func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) { + var err error + // Construct the URL with the path parameter + url := fmt.Sprintf("%s/%s", config.TrackURL, publicKey) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-GE-KEY", "xd") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + errResponse.Description = err.Error() + return nil, err + } + if resp.StatusCode >= http.StatusBadRequest { + err := json.Unmarshal([]byte(body), &errResponse) + if err != nil { + return nil, err + } + return nil, errors.New(errResponse.Description) + } + err = json.Unmarshal([]byte(body), &okResponse) + if err != nil { + return nil, err + } + if len(okResponse.Result) == 0 { + return nil, errors.New("Empty api result") + } + return &okResponse, nil + } // CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint. // Parameters: // - publicKey: The public key associated with the account whose balance needs to be checked. -func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) { +func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) { resp, err := http.Get(config.BalanceURL + publicKey) if err != nil { return nil, err @@ -81,22 +132,43 @@ func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceRespons // If there is an error during the request or processing, this will be nil. // - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. // If no error occurs, this will be nil. -func (as *AccountService) CreateAccount() (*models.AccountResponse, error) { - resp, err := http.Post(config.CreateAccountURL, "application/json", nil) +func (as *AccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) { + var err error + + // Create a new request + req, err := http.NewRequest("POST", config.CreateAccountURL, nil) if err != nil { return nil, err } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-GE-KEY", "xd") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + errResponse.Description = err.Error() + return nil, err + } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } - var accountResp models.AccountResponse - err = json.Unmarshal(body, &accountResp) + if resp.StatusCode >= http.StatusBadRequest { + err := json.Unmarshal([]byte(body), &errResponse) + if err != nil { + return nil, err + } + return nil, errors.New(errResponse.Description) + } + err = json.Unmarshal([]byte(body), &okResponse) if err != nil { return nil, err } - return &accountResp, nil + if len(okResponse.Result) == 0 { + return nil, errors.New("Empty api result") + } + return &okResponse, nil } // FetchVouchers retrieves the token holdings for a given public key from the custodial holdings API endpoint @@ -116,22 +188,16 @@ func (as *AccountService) FetchVouchers(publicKey string) (*models.VoucherHoldin return &holdings, nil } -func (tas *TestAccountService) CreateAccount() (*models.AccountResponse, error) { - return &models.AccountResponse{ - Ok: true, - Result: struct { - CustodialId json.Number `json:"custodialId"` - PublicKey string `json:"publicKey"` - TrackingId string `json:"trackingId"` - }{ - CustodialId: json.Number("182"), - PublicKey: "0x48ADca309b5085852207FAaf2816eD72B52F527C", - TrackingId: "28ebe84d-b925-472c-87ae-bbdfa1fb97be", - }, +func (tas *TestAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) { + return &api.OKResponse{ + Ok: true, + Description: "Account creation request received successfully", + Result: map[string]any{"publicKey": "0x48ADca309b5085852207FAaf2816eD72B52F527C", "trackingId": "28ebe84d-b925-472c-87ae-bbdfa1fb97be"}, }, nil + } -func (tas *TestAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) { +func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) { balanceResponse := &models.BalanceResponse{ Ok: true, Result: struct { @@ -142,11 +208,20 @@ func (tas *TestAccountService) CheckBalance(publicKey string) (*models.BalanceRe Nonce: json.Number("0"), }, } - return balanceResponse, nil } -func (tas *TestAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { +func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*api.OKResponse, error) { + return &api.OKResponse{ + Ok: true, + Description: "Account creation succeeded", + Result: map[string]any{ + "active": true, + }, + }, nil +} + +func (tas *TestAccountService) CheckAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResponse, error) { trackResponse := &models.TrackStatusResponse{ Ok: true, Result: struct { diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 56080bb..ec0a5b3 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -10,6 +10,7 @@ import ( "strings" "git.defalsify.org/vise.git/asm" + "github.com/grassrootseconomics/eth-custodial/pkg/api" "git.defalsify.org/vise.git/cache" "git.defalsify.org/vise.git/db" @@ -29,6 +30,8 @@ var ( logg = logging.NewVanilla().WithDomain("ussdmenuhandler") scriptDir = path.Join("services", "registration") translationDir = path.Join(scriptDir, "locale") + okResponse *api.OKResponse + errResponse *api.ErrResponse ) // FlagManager handles centralized flag management @@ -138,13 +141,18 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r } func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error { - accountResp, err := h.accountService.CreateAccount() - data := map[utils.DataTyp]string{ - utils.DATA_TRACKING_ID: accountResp.Result.TrackingId, - utils.DATA_PUBLIC_KEY: accountResp.Result.PublicKey, - utils.DATA_CUSTODIAL_ID: accountResp.Result.CustodialId.String(), + flag_account_created, _ := h.flagManager.GetFlag("flag_account_created") + okResponse, err := h.accountService.CreateAccount(ctx) + if err != nil { + return err } + trackingId := okResponse.Result["trackingId"].(string) + publicKey := okResponse.Result["publicKey"].(string) + data := map[utils.DataTyp]string{ + utils.DATA_TRACKING_ID: trackingId, + utils.DATA_PUBLIC_KEY: publicKey, + } for key, value := range data { store := h.userdataStore err := store.WriteEntry(ctx, sessionId, key, []byte(value)) @@ -152,9 +160,8 @@ func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, r return err } } - flag_account_created, _ := h.flagManager.GetFlag("flag_account_created") res.FlagSet = append(res.FlagSet, flag_account_created) - return err + return nil } @@ -355,7 +362,6 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou if !ok { return res, fmt.Errorf("missing session") } - if len(input) == 4 { yob := string(input) store := h.userdataStore @@ -398,7 +404,6 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re if !ok { return res, fmt.Errorf("missing session") } - gender := strings.Split(symbol, "_")[1] store := h.userdataStore err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender)) @@ -417,7 +422,6 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte) if !ok { return res, fmt.Errorf("missing session") } - if len(input) > 0 { offerings := string(input) store := h.userdataStore @@ -443,7 +447,6 @@ func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byt // ResetAccountAuthorized resets the account authorization flag after a successful PIN entry. func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") res.FlagReset = append(res.FlagReset, flag_account_authorized) @@ -453,12 +456,10 @@ func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input // CheckIdentifier retrieves the PublicKey from the JSON data file. func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } - store := h.userdataStore publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) @@ -472,12 +473,10 @@ func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result var err error - sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } - flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") @@ -529,28 +528,21 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b return res, fmt.Errorf("missing session") } store := h.userdataStore - trackingId, err := store.ReadEntry(ctx, sessionId, utils.DATA_TRACKING_ID) + publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) if err != nil { return res, err } - - accountStatus, err := h.accountService.CheckAccountStatus(string(trackingId)) + okResponse, err = h.accountService.TrackAccountStatus(ctx, string(publicKey)) if err != nil { - fmt.Println("Error checking account status:", err) - return res, err - } - if !accountStatus.Ok { res.FlagSet = append(res.FlagSet, flag_api_error) return res, err } res.FlagReset = append(res.FlagReset, flag_api_error) - status := accountStatus.Result.Transaction.Status - - err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)) - if err != nil { - return res, nil + isActive := okResponse.Result["active"].(bool) + if !ok { + return res, err } - if accountStatus.Result.Transaction.Status == "SUCCESS" { + if isActive { res.FlagSet = append(res.FlagSet, flag_account_success) res.FlagReset = append(res.FlagReset, flag_account_pending) } else { @@ -680,7 +672,7 @@ func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input return res, err } - balanceResponse, err := h.accountService.CheckBalance(string(publicKey)) + balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey)) if err != nil { return res, nil } diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index 97df63e..9bcee63 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -20,6 +20,7 @@ import ( "git.grassecon.net/urdt/ussd/internal/models" "git.grassecon.net/urdt/ussd/internal/utils" "github.com/alecthomas/assert/v2" + "github.com/grassrootseconomics/eth-custodial/pkg/api" testdataloader "github.com/peteole/testdata-loader" "github.com/stretchr/testify/require" ) @@ -72,68 +73,74 @@ func TestCreateAccount(t *testing.T) { if err != nil { t.Logf(err.Error()) } - // Create required mocks - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - expectedResult := resource.Result{} - accountCreatedFlag, err := fm.GetFlag("flag_account_created") - + flag_account_created, err := fm.GetFlag("flag_account_created") if err != nil { t.Logf(err.Error()) } - expectedResult.FlagSet = append(expectedResult.FlagSet, accountCreatedFlag) - // Define session ID and mock data sessionId := "session123" - typ := utils.DATA_ACCOUNT_CREATED - fakeError := db.ErrNotFound{} - // Create context with session ID + notFoundErr := db.ErrNotFound{} ctx := context.WithValue(context.Background(), "SessionId", sessionId) - // Define expected interactions with the mock - mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte("123"), fakeError) - expectedAccountResp := &models.AccountResponse{ - Ok: true, - Result: struct { - CustodialId json.Number `json:"custodialId"` - PublicKey string `json:"publicKey"` - TrackingId string `json:"trackingId"` - }{ - CustodialId: "12", - PublicKey: "0x8E0XSCSVA", - TrackingId: "d95a7e83-196c-4fd0-866fSGAGA", + tests := []struct { + name string + serverResponse *api.OKResponse + expectedResult resource.Result + }{ + { + name: "Test account creation success", + serverResponse: &api.OKResponse{ + Ok: true, + Description: "Account creation successed", + Result: map[string]any{ + "trackingId": "1234567890", + "publicKey": "1235QERYU", + }, + }, + expectedResult: resource.Result{ + FlagSet: []uint32{flag_account_created}, + }, }, } - mockCreateAccountService.On("CreateAccount").Return(expectedAccountResp, nil) - data := map[utils.DataTyp]string{ - utils.DATA_TRACKING_ID: expectedAccountResp.Result.TrackingId, - utils.DATA_PUBLIC_KEY: expectedAccountResp.Result.PublicKey, - utils.DATA_CUSTODIAL_ID: expectedAccountResp.Result.CustodialId.String(), + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + mockDataStore := new(mocks.MockUserDataStore) + mockCreateAccountService := new(mocks.MockAccountService) + + // Create a Handlers instance with the mock data store + h := &Handlers{ + userdataStore: mockDataStore, + accountService: mockCreateAccountService, + flagManager: fm.parser, + } + + data := map[utils.DataTyp]string{ + utils.DATA_TRACKING_ID: tt.serverResponse.Result["trackingId"].(string), + utils.DATA_PUBLIC_KEY: tt.serverResponse.Result["publicKey"].(string), + } + + mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACCOUNT_CREATED).Return([]byte(""), notFoundErr) + mockCreateAccountService.On("CreateAccount").Return(tt.serverResponse, nil) + + for key, value := range data { + mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil) + } + + // Call the method you want to test + res, err := h.CreateAccount(ctx, "create_account", []byte("some-input")) + + // Assert that no errors occurred + assert.NoError(t, err) + + // Assert that the account created flag has been set to the result + assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result") + + // Assert that expectations were met + mockDataStore.AssertExpectations(t) + }) } - - for key, value := range data { - mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil) - } - - // Create a Handlers instance with the mock data store - h := &Handlers{ - userdataStore: mockDataStore, - accountService: mockCreateAccountService, - flagManager: fm.parser, - } - - // Call the method you want to test - res, err := h.CreateAccount(ctx, "create_account", []byte("some-input")) - - // Assert that no errors occurred - assert.NoError(t, err) - - //Assert that the account created flag has been set to the result - assert.Equal(t, res, expectedResult, "Expected result should be equal to the actual result") - - // Assert that expectations were met - mockDataStore.AssertExpectations(t) } func TestWithPersister(t *testing.T) { @@ -1037,12 +1044,20 @@ func TestCheckAccountStatus(t *testing.T) { tests := []struct { name string input []byte + serverResponse *api.OKResponse response *models.TrackStatusResponse expectedResult resource.Result }{ { - name: "Test when account status is Success", + name: "Test when account is on the Sarafu network", input: []byte("TrackingId1234"), + serverResponse: &api.OKResponse{ + Ok: true, + Description: "Account creation succeeded", + Result: map[string]any{ + "active": true, + }, + }, response: &models.TrackStatusResponse{ Ok: true, Result: struct { @@ -1069,17 +1084,7 @@ func TestCheckAccountStatus(t *testing.T) { }, }, { - name: "Test when fetching account status is not Success", - input: []byte("TrackingId1234"), - response: &models.TrackStatusResponse{ - Ok: false, - }, - expectedResult: resource.Result{ - FlagSet: []uint32{flag_api_error}, - }, - }, - { - name: "Test when checking account status api call is a SUCCESS but an account is not yet ready", + name: "Test when the account is not yet on the sarafu network", input: []byte("TrackingId1234"), response: &models.TrackStatusResponse{ Ok: true, @@ -1094,13 +1099,20 @@ func TestCheckAccountStatus(t *testing.T) { }{ Transaction: models.Transaction{ CreatedAt: time.Now(), - Status: "IN_NETWORK", + Status: "SUCCESS", TransferValue: json.Number("0.5"), TxHash: "0x123abc456def", TxType: "transfer", }, }, }, + serverResponse: &api.OKResponse{ + Ok: true, + Description: "Account creation succeeded", + Result: map[string]any{ + "active": false, + }, + }, expectedResult: resource.Result{ FlagSet: []uint32{flag_account_pending}, FlagReset: []uint32{flag_api_error, flag_account_success}, @@ -1120,9 +1132,10 @@ func TestCheckAccountStatus(t *testing.T) { status := tt.response.Result.Transaction.Status // Define expected interactions with the mock - mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TRACKING_ID).Return(tt.input, nil) + mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.input, nil) mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.response, nil) + mockCreateAccountService.On("TrackAccountStatus", string(tt.input)).Return(tt.serverResponse, nil) mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)).Return(nil).Maybe() // Call the method under test diff --git a/internal/mocks/servicemock.go b/internal/mocks/servicemock.go index f41f6c6..6a217ce 100644 --- a/internal/mocks/servicemock.go +++ b/internal/mocks/servicemock.go @@ -1,7 +1,10 @@ package mocks import ( + "context" + "git.grassecon.net/urdt/ussd/internal/models" + "github.com/grassrootseconomics/eth-custodial/pkg/api" "github.com/stretchr/testify/mock" ) @@ -10,21 +13,27 @@ type MockAccountService struct { mock.Mock } -func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) { +func (m *MockAccountService) CreateAccount(ctx context.Context) (*api.OKResponse, error) { args := m.Called() - return args.Get(0).(*models.AccountResponse), args.Error(1) + return args.Get(0).(*api.OKResponse), args.Error(1) } -func (m *MockAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) { +func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResponse, error) { args := m.Called(publicKey) return args.Get(0).(*models.BalanceResponse), args.Error(1) } -func (m *MockAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { +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(publicKey string) (*models.VoucherHoldingResponse, error) { args := m.Called(publicKey) return args.Get(0).(*models.VoucherHoldingResponse), args.Error(1) diff --git a/internal/models/accountresponse.go b/internal/models/accountresponse.go index 1422a20..efcc30b 100644 --- a/internal/models/accountresponse.go +++ b/internal/models/accountresponse.go @@ -1,15 +1,10 @@ package models -import ( - "encoding/json" - -) - type AccountResponse struct { - Ok bool `json:"ok"` - Result struct { - CustodialId json.Number `json:"custodialId"` - PublicKey string `json:"publicKey"` - TrackingId string `json:"trackingId"` + Ok bool `json:"ok"` + Description string `json:"description"` // Include the description field + Result struct { + PublicKey string `json:"publicKey"` + TrackingId string `json:"trackingId"` } `json:"result"` -} \ No newline at end of file +} diff --git a/internal/models/trackstatusresponse.go b/internal/models/trackstatusresponse.go index 1629a7c..6b96d90 100644 --- a/internal/models/trackstatusresponse.go +++ b/internal/models/trackstatusresponse.go @@ -4,7 +4,6 @@ import ( "encoding/json" "time" ) - type Transaction struct { CreatedAt time.Time `json:"createdAt"` Status string `json:"status"`