commit 134989a92de16354c8ae5699342eaba2ce718a7f Author: lash Date: Sat Jan 11 17:29:16 2025 +0000 Initial commit, move api from monorepo diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..255ee42 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.grassecon.net/grassrootseconomics/sarafu-api + +go 1.23.4 + +require ( + git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250111151614-46bf21b7b8bd + github.com/grassrootseconomics/ussd-data-service v1.2.0-beta + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/objx v0.5.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d0ddcf8 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250111151614-46bf21b7b8bd h1:mKCov8udBJBQuMF3aFg38SkClL8OvAUZmtArNzgIPak= +git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250111151614-46bf21b7b8bd/go.mod h1:E6W7ZOa7ZvVr0Bc5ot0LNSwpSPYq4hXlAIvEPy3AJ7U= +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/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk= +github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0= +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/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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/models/account_response.go b/models/account_response.go new file mode 100644 index 0000000..dc8e758 --- /dev/null +++ b/models/account_response.go @@ -0,0 +1,6 @@ +package models + +type AccountResult struct { + PublicKey string `json:"publicKey"` + TrackingId string `json:"trackingId"` +} diff --git a/models/balance_response.go b/models/balance_response.go new file mode 100644 index 0000000..88e9ce9 --- /dev/null +++ b/models/balance_response.go @@ -0,0 +1,8 @@ +package models + +import "encoding/json" + +type BalanceResult struct { + Balance string `json:"balance"` + Nonce json.Number `json:"nonce"` +} diff --git a/models/profile.go b/models/profile.go new file mode 100644 index 0000000..d698318 --- /dev/null +++ b/models/profile.go @@ -0,0 +1,18 @@ +package models + +type Profile struct { + ProfileItems []string + Max int +} + +func (p *Profile) InsertOrShift(index int, value string) { + if index < len(p.ProfileItems) { + p.ProfileItems = append(p.ProfileItems[:index], value) + } else { + for len(p.ProfileItems) < index { + p.ProfileItems = append(p.ProfileItems, "0") + } + p.ProfileItems = append(p.ProfileItems, "0") + p.ProfileItems[index] = value + } +} diff --git a/models/token_transfer_response.go b/models/token_transfer_response.go new file mode 100644 index 0000000..b4d6dc3 --- /dev/null +++ b/models/token_transfer_response.go @@ -0,0 +1,5 @@ +package models + +type TokenTransferResponse struct { + TrackingId string `json:"trackingId"` +} diff --git a/models/track_status_response.go b/models/track_status_response.go new file mode 100644 index 0000000..0c3c230 --- /dev/null +++ b/models/track_status_response.go @@ -0,0 +1,18 @@ +package models + +import ( + "encoding/json" + "time" +) + +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 { + Active bool `json:"active"` +} diff --git a/models/voucher_data_result.go b/models/voucher_data_result.go new file mode 100644 index 0000000..9a10831 --- /dev/null +++ b/models/voucher_data_result.go @@ -0,0 +1,10 @@ +package models + +type VoucherDataResult struct { + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimals int `json:"tokenDecimals"` + SinkAddress string `json:"sinkAddress"` + TokenCommodity string `json:"tokenCommodity"` + TokenLocation string `json:"tokenLocation"` +} diff --git a/remote/account_service.go b/remote/account_service.go new file mode 100644 index 0000000..3fbaaf0 --- /dev/null +++ b/remote/account_service.go @@ -0,0 +1,294 @@ +package remote + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "log" + "net/http" + "net/url" + + "git.grassecon.net/grassrootseconomics/visedriver/config" + "git.grassecon.net/grassrootseconomics/visedriver/models" + "github.com/grassrootseconomics/eth-custodial/pkg/api" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +type AccountServiceInterface interface { + CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) + CreateAccount(ctx context.Context) (*models.AccountResult, error) + TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) + FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) + FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) + VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) + TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) + CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) +} + +type AccountService struct { +} + +// 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 +// AccountResponse struct can be used here to check the account status during a transaction. +// +// 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) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { + var r models.TrackStatusResult + + ep, err := url.JoinPath(config.TrackURL, publicKey) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, 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(ctx context.Context, publicKey string) (*models.BalanceResult, error) { + var balanceResult models.BalanceResult + + ep, err := url.JoinPath(config.BalanceURL, publicKey) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doRequest(ctx, req, &balanceResult) + return &balanceResult, err +} + +// CreateAccount creates a new account in the custodial system. +// Returns: +// - *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. +// - 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(ctx context.Context) (*models.AccountResult, error) { + var r models.AccountResult + // Create a new request + req, err := http.NewRequest("POST", config.CreateAccountURL, nil) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +// FetchVouchers retrieves the token holdings for a given public key from the data indexer 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 struct { + Holdings []dataserviceapi.TokenHoldings `json:"holdings"` + } + + ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return r.Holdings, nil +} + +// FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint +// Parameters: +// - publicKey: The public key associated with the account. +func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { + var r struct { + Transfers []dataserviceapi.Last10TxResponse `json:"transfers"` + } + + ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return r.Transfers, nil +} + +// VoucherData retrieves voucher metadata from the data indexer API endpoint. +// Parameters: +// - address: The voucher address. +func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { + var r struct { + TokenDetails models.VoucherDataResult `json:"tokenDetails"` + } + + ep, err := url.JoinPath(config.VoucherDataURL, address) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doRequest(ctx, req, &r) + return &r.TokenDetails, err +} + +// TokenTransfer creates a new token transfer in the custodial system. +// Returns: +// - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId. +// 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) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { + var r models.TokenTransferResponse + + // Create request payload + payload := map[string]string{ + "amount": amount, + "from": from, + "to": to, + "tokenAddress": tokenAddress, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + // Create a new request + req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes)) + if err != nil { + return nil, err + } + _, err = doRequest(ctx, req, &r) + if err != nil { + return nil, err + } + + return &r, nil +} + +// CheckAliasAddress retrieves the address of an alias from the API endpoint. +// Parameters: +// - alias: The alias of the user. +func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { + var r dataserviceapi.AliasAddress + + ep, err := url.JoinPath(config.CheckAliasURL, alias) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", ep, nil) + if err != nil { + return nil, err + } + + _, err = doRequest(ctx, req, &r) + return &r, err +} + +func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { + var okResponse api.OKResponse + var errResponse api.ErrResponse + + req.Header.Set("Authorization", "Bearer "+config.BearerToken) + req.Header.Set("Content-Type", "application/json") + + logRequestDetails(req) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error()) + errResponse.Description = err.Error() + return nil, err + } + defer resp.Body.Close() + + log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type")) + body, err := io.ReadAll(resp.Body) + if err != nil { + 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") + } + + v, err := json.Marshal(okResponse.Result) + if err != nil { + return nil, err + } + + err = json.Unmarshal(v, &rcpt) + return &okResponse, err +} + +func logRequestDetails(req *http.Request) { + var bodyBytes []byte + contentType := req.Header.Get("Content-Type") + if req.Body != nil { + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + log.Printf("Error reading request body: %s", err) + return + } + req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } else { + bodyBytes = []byte("-") + } + + log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes)) +} diff --git a/testutil/mocks/service_mock.go b/testutil/mocks/service_mock.go new file mode 100644 index 0000000..9033376 --- /dev/null +++ b/testutil/mocks/service_mock.go @@ -0,0 +1,54 @@ +package mocks + +import ( + "context" + + "git.grassecon.net/grassrootseconomics/visedriver/models" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" + "github.com/stretchr/testify/mock" +) + +// MockAccountService implements AccountServiceInterface for testing +type MockAccountService struct { + mock.Mock +} + +func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { + 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) (*models.TrackStatusResult, error) { + args := m.Called(trackingId) + return args.Get(0).(*models.TrackStatusResult), args.Error(1) +} + +func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + args := m.Called(publicKey) + return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1) +} + +func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { + args := m.Called(publicKey) + return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1) +} + +func (m *MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { + args := m.Called(address) + return args.Get(0).(*models.VoucherDataResult), args.Error(1) +} + +func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { + args := m.Called() + return args.Get(0).(*models.TokenTransferResponse), args.Error(1) +} + +func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { + args := m.Called(alias) + return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1) +} diff --git a/testutil/testservice/account_service.go b/testutil/testservice/account_service.go new file mode 100644 index 0000000..300ef52 --- /dev/null +++ b/testutil/testservice/account_service.go @@ -0,0 +1,62 @@ +package testservice + +import ( + "context" + "encoding/json" + + "git.grassecon.net/grassrootseconomics/visedriver/models" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +type TestAccountService struct { +} + +func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { + return &models.AccountResult{ + TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d", + PublicKey: "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD", + }, nil +} + +func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { + balanceResponse := &models.BalanceResult{ + Balance: "0.003 CELO", + Nonce: json.Number("0"), + } + return balanceResponse, nil +} + +func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { + return &models.TrackStatusResult{ + Active: true, + }, nil +} + +func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { + return []dataserviceapi.TokenHoldings{ + dataserviceapi.TokenHoldings{ + ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", + TokenSymbol: "SRF", + TokenDecimals: "6", + Balance: "2745987", + }, + }, nil +} + +func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { + return []dataserviceapi.Last10TxResponse{}, nil +} + +func (m TestAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { + return &models.VoucherDataResult{}, nil +} + +func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { + return &models.TokenTransferResponse{ + TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af", + }, nil +} + +func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { + return &dataserviceapi.AliasAddress{}, nil +}