Skip to content

Commit 9eb67d7

Browse files
authored
Merge pull request #549 from hyperweb-io/anmol/faucet-postcheck
faucet postcheck
2 parents 7d966f5 + 47e9904 commit 9eb67d7

File tree

7 files changed

+222
-19
lines changed

7 files changed

+222
-19
lines changed

starship/charts/devnet/templates/chains/cosmos/genesis.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ spec:
394394
httpGet:
395395
path: /status
396396
port: {{ $chain.faucet.ports.rest }}
397-
initialDelaySeconds: 15
397+
initialDelaySeconds: 30
398398
periodSeconds: 10
399399
{{- else if eq $chain.faucet.type "starship" }}
400400
- name: faucet
@@ -463,7 +463,7 @@ spec:
463463
httpGet:
464464
path: /status
465465
port: {{ $chain.faucet.ports.rest }}
466-
initialDelaySeconds: 10
466+
initialDelaySeconds: 30
467467
periodSeconds: 10
468468
{{- end }}
469469
{{- end }}

starship/faucet/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ func NewDefaultConfig() *Config {
2020
RefillFactor: 8,
2121
RefillThreshold: 20,
2222
RefillEpoch: 100,
23+
CreditCheckEpoch: 3,
24+
CreditCheckRetry: 3,
2325
Verbose: true,
2426
}
2527
}
@@ -55,6 +57,10 @@ type Config struct {
5557
RefillEpoch int `name:"refill-epoch" json:"refill_epoch" env:"REFILL_EPOCH" usage:"after every epoch, holder will distrubute tokens to distributor addresses if required"`
5658
// CreditCoins is comma seperated list of amount and denom of tokens to be transfered
5759
CreditCoins string `name:"credit-coins" json:"credit_coins" env:"CREDIT_COINS" usage:"comma seperated list of amount and denom of tokens to be transfered. eg: 10000000uosmo,1000000uion"`
60+
// CreditCheckEpoch is the time (in secs) to sleep, after which holder will check for credit request
61+
CreditCheckEpoch int `name:"credit-check-epoch" json:"credit_check_epoch" env:"CREDIT_CHECK_EPOCH" usage:"after every epoch, holder will check for credit request"`
62+
// CreditCheckRetry is the number of retries to check for credit request
63+
CreditCheckRetry int `name:"credit-check-retry" json:"credit_check_retry" env:"CREDIT_CHECK_RETRY" usage:"number of retries to check for credit request"`
5864
// Mnemonic is the mnemonic of the address used as the faucet address
5965
Mnemonic string `name:"mnemonic" json:"mnemonic" env:"MNEMONIC" usage:"mnemonic of the address used as the faucet address"`
6066
// Verbose switches on debug logging

starship/faucet/distributor.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,23 @@ func (a *Account) String() string {
276276
return fmt.Sprintf("name: %s, addr: %s", a.Name, a.Address)
277277
}
278278

279+
// queryTx queries the tx based on the txhash
280+
func (a *Account) queryTx(txhash string) (map[string]interface{}, error) {
281+
cmdStr := fmt.Sprintf("%s q tx %s --output=json", a.config.ChainBinary, txhash)
282+
output, err := runCommand(cmdStr)
283+
if err != nil {
284+
return nil, err
285+
}
286+
287+
txMap := map[string]interface{}{}
288+
err = json.Unmarshal(output, &txMap)
289+
if err != nil {
290+
return nil, err
291+
}
292+
293+
return txMap, nil
294+
}
295+
279296
// sendTokens performs chain binary send txn from account
280297
func (a *Account) sendTokens(address string, denom string, amount string) error {
281298
ok := a.mu.TryLock()
@@ -284,7 +301,7 @@ func (a *Account) sendTokens(address string, denom string, amount string) error
284301
}
285302
defer a.mu.Unlock()
286303

287-
args := fmt.Sprintf("--chain-id=%s --fees=%s --keyring-backend=test --gas=auto --gas-adjustment=1.5 --yes --node=%s", a.config.ChainId, a.config.ChainFees, a.config.ChainRPCEndpoint)
304+
args := fmt.Sprintf("--chain-id=%s --fees=%s --keyring-backend=test --gas=auto --gas-adjustment=1.5 --output=json --yes --node=%s", a.config.ChainId, a.config.ChainFees, a.config.ChainRPCEndpoint)
288305
cmdStr := fmt.Sprintf("%s tx bank send %s %s %s%s %s", a.config.ChainBinary, a.Address, address, amount, denom, args)
289306
output, err := runCommand(cmdStr)
290307
if err != nil {
@@ -293,6 +310,30 @@ func (a *Account) sendTokens(address string, denom string, amount string) error
293310
}
294311
a.logger.Info("ran cmd to send tokens", zap.String("cmd", cmdStr), zap.String("stdout", string(output)))
295312

313+
// Find the JSON line and extract the txhash using a regular expression
314+
txhash, err := extractTxHash(string(output))
315+
if err != nil {
316+
a.logger.Error("failed to extract txhash", zap.Error(err))
317+
return err
318+
}
319+
320+
a.logger.Debug("send tokens txhash", zap.String("txhash", txhash))
321+
322+
// query tx to check if the tx was successful
323+
txMap, err := a.queryTx(txhash)
324+
if err != nil {
325+
return err
326+
}
327+
328+
// check if the tx has the event
329+
err = hasEvent(txMap, "transfer")
330+
if err != nil {
331+
a.logger.Error("transfer event not found in tx",
332+
zap.String("txhash", txhash),
333+
zap.Any("txMap", txMap))
334+
return err
335+
}
336+
296337
return nil
297338
}
298339

starship/faucet/handler.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package main
22

33
import (
44
"context"
5+
"fmt"
6+
"math/big"
7+
"time"
58

9+
"go.uber.org/zap"
610
"google.golang.org/protobuf/types/known/emptypb"
711

812
pb "github.com/hyperweb-io/starship/faucet/faucet"
@@ -37,11 +41,64 @@ func (a *AppServer) Status(ctx context.Context, _ *emptypb.Empty) (*pb.State, er
3741
return state, nil
3842
}
3943

44+
func (a *AppServer) getBalance(address, denom string) (*big.Int, error) {
45+
account := &Account{config: a.config, logger: a.logger, Address: address}
46+
coin, err := account.GetBalanceByDenom(denom)
47+
if err != nil {
48+
// Log the error, but don't return it
49+
a.logger.Debug("Error getting balance, assuming new account", zap.Error(err))
50+
return new(big.Int), nil // Return 0 balance
51+
}
52+
balance, ok := new(big.Int).SetString(coin.Amount, 10)
53+
if !ok {
54+
return nil, fmt.Errorf("failed to parse balance")
55+
}
56+
return balance, nil
57+
}
58+
4059
func (a *AppServer) Credit(ctx context.Context, requestCredit *pb.RequestCredit) (*pb.ResponseCredit, error) {
41-
err := a.distributor.SendTokens(requestCredit.GetAddress(), requestCredit.GetDenom())
60+
// Get initial balance before sending tokens
61+
initialBalance, err := a.getBalance(requestCredit.GetAddress(), requestCredit.GetDenom())
62+
if err != nil {
63+
return nil, fmt.Errorf("failed to get initial balance: %v", err)
64+
}
65+
66+
err = a.distributor.SendTokens(requestCredit.GetAddress(), requestCredit.GetDenom())
4267
if err != nil {
4368
return nil, err
4469
}
4570

71+
// Check balance after transfer
72+
confirmed, err := a.confirmBalanceUpdate(requestCredit.GetAddress(), requestCredit.GetDenom(), initialBalance)
73+
if err != nil {
74+
return &pb.ResponseCredit{Status: fmt.Sprintf("error: %v", err)}, err
75+
}
76+
if !confirmed {
77+
return &pb.ResponseCredit{Status: "error: failed to confirm balance update (timeout reached)"}, nil
78+
}
79+
4680
return &pb.ResponseCredit{Status: "ok"}, nil
4781
}
82+
83+
func (a *AppServer) confirmBalanceUpdate(address, denom string, initialBalance *big.Int) (bool, error) {
84+
expectedIncrease, ok := new(big.Int).SetString(a.distributor.CreditCoins.GetDenomAmount(denom), 10)
85+
if !ok {
86+
return false, fmt.Errorf("failed to parse expected amount")
87+
}
88+
89+
expectedFinalBalance := new(big.Int).Add(initialBalance, expectedIncrease)
90+
91+
for i := 0; i < a.config.CreditCheckRetry; i++ { // Try x times with t-second intervals
92+
currentBalance, err := a.getBalance(address, denom)
93+
if err != nil {
94+
return false, err
95+
}
96+
if currentBalance.Cmp(expectedFinalBalance) >= 0 {
97+
return true, nil
98+
}
99+
if i < 2 {
100+
time.Sleep(time.Duration(a.config.CreditCheckEpoch) * time.Second)
101+
}
102+
}
103+
return false, nil
104+
}

starship/faucet/utils.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"os/exec"
8+
"regexp"
89
)
910

1011
// runCommand will run the cmdStr as exec and return the results as a []byte
@@ -22,3 +23,53 @@ func runCommand(cmdStr string) ([]byte, error) {
2223

2324
return outb.Bytes(), nil
2425
}
26+
27+
// extractTxHash uses a regular expression to extract the txhash from the output
28+
func extractTxHash(output string) (string, error) {
29+
// Regular expression to match the txhash value in the JSON output
30+
re := regexp.MustCompile(`"txhash":"([0-9A-Fa-f]{64})"`)
31+
match := re.FindStringSubmatch(output)
32+
if len(match) < 2 {
33+
return "", fmt.Errorf("txhash not found in output")
34+
}
35+
return match[1], nil
36+
}
37+
38+
// hasEvent checks for a specific event type in the transaction results.
39+
// It first tries to fetch the events either from txMap["events"] or from txMap["logs"] and then checks for the event type.
40+
func hasEvent(txResults map[string]interface{}, eventType string) error {
41+
var events []interface{}
42+
43+
// Attempt to fetch events directly from txMap["events"]
44+
if directEvents, ok := txResults["events"].([]interface{}); ok && len(directEvents) > 0 {
45+
events = directEvents
46+
} else if logs, ok := txResults["logs"].([]interface{}); ok && len(logs) > 0 {
47+
// If no direct events, attempt to fetch events from logs
48+
for _, log := range logs {
49+
logMap, ok := log.(map[string]interface{})
50+
if !ok {
51+
continue
52+
}
53+
if logEvents, ok := logMap["events"].([]interface{}); ok && len(logEvents) > 0 {
54+
events = append(events, logEvents...)
55+
}
56+
}
57+
}
58+
59+
// If events are found, check for the specific eventType
60+
if len(events) > 0 {
61+
for _, event := range events {
62+
eventMap, ok := event.(map[string]interface{})
63+
if !ok {
64+
continue
65+
}
66+
if eventMap["type"] == eventType {
67+
return nil
68+
}
69+
}
70+
return fmt.Errorf("event %s not found in transaction events", eventType)
71+
}
72+
73+
// If no events were found in either place
74+
return fmt.Errorf("no events found in transaction")
75+
}

starship/tests/e2e/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ delete:
3636

3737
.PHONY: test
3838
test:
39-
TEST_CONFIG_FILE=$(HELM_FILE) go test -v ./... -count=1
39+
TEST_CONFIG_FILE=$(HELM_FILE) CGO_ENABLED=0 go test -v ./... -count=1
4040

4141
###############################################################################
4242
### Port forward ###

starship/tests/e2e/faucet_test.go

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7-
pb "github.com/hyperweb-io/starship/registry/registry"
87
"net/http"
98
urlpkg "net/url"
109
"strconv"
11-
"time"
10+
11+
pb "github.com/hyperweb-io/starship/registry/registry"
1212
)
1313

1414
func (s *TestSuite) MakeFaucetRequest(chain *Chain, req *http.Request, unmarshal map[string]interface{}) {
@@ -115,6 +115,28 @@ func (s *TestSuite) getAccountBalance(chain *Chain, address string, denom string
115115
return float64(0)
116116
}
117117

118+
func (s *TestSuite) creditAccount(chain *Chain, addr, denom string) error {
119+
body := map[string]string{
120+
"denom": denom,
121+
"address": addr,
122+
}
123+
postBody, err := json.Marshal(body)
124+
if err != nil {
125+
return err
126+
}
127+
resp, err := http.Post(
128+
fmt.Sprintf("http://0.0.0.0:%d/credit", chain.Ports.Faucet),
129+
"application/json",
130+
bytes.NewBuffer(postBody))
131+
if err != nil {
132+
return err
133+
}
134+
if resp.StatusCode != 200 {
135+
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
136+
}
137+
return nil
138+
}
139+
118140
func (s *TestSuite) TestFaucet_Credit() {
119141
s.T().Log("running test for /credit endpoint for faucet")
120142

@@ -132,20 +154,9 @@ func (s *TestSuite) TestFaucet_Credit() {
132154
addr := getAddressFromType(chain.Name)
133155
beforeBalance := s.getAccountBalance(chain, addr, denom)
134156

135-
body := map[string]string{
136-
"denom": denom,
137-
"address": addr,
138-
}
139-
postBody, err := json.Marshal(body)
140-
s.Require().NoError(err)
141-
resp, err := http.Post(
142-
fmt.Sprintf("http://0.0.0.0:%d/credit", chain.Ports.Faucet),
143-
"application/json",
144-
bytes.NewBuffer(postBody))
157+
err := s.creditAccount(chain, addr, denom)
145158
s.Require().NoError(err)
146-
s.Require().Equal(200, resp.StatusCode)
147159

148-
time.Sleep(4 * time.Second)
149160
afterBalance := s.getAccountBalance(chain, addr, denom)
150161
s.T().Log("address:", addr, "after balance: ", afterBalance, "before balance:", beforeBalance)
151162
// note sometimes expected difference is 9x expected value (bug due to using holder address for test)
@@ -154,3 +165,40 @@ func (s *TestSuite) TestFaucet_Credit() {
154165
})
155166
}
156167
}
168+
169+
func (s *TestSuite) TestFaucet_Credit_MultipleRequests() {
170+
s.T().Log("running test for multiple requests to /credit endpoint for faucet")
171+
172+
// expected amount to be credited via faucet
173+
expCreditedAmt := float64(10000000000)
174+
175+
for _, chain := range s.config.Chains {
176+
s.Run(fmt.Sprintf("multiple faucet requests test for: %s", chain.ID), func() {
177+
if chain.Ports.Faucet == 0 {
178+
s.T().Skip("faucet not exposed via ports")
179+
}
180+
181+
// fetch denom and address from an account on chain
182+
denom := s.getChainDenoms(chain)
183+
addr := getAddressFromType(chain.Name)
184+
beforeBalance := s.getAccountBalance(chain, addr, denom)
185+
186+
// Send multiple requests
187+
numRequests := 3
188+
for i := 0; i < numRequests; i++ {
189+
err := s.creditAccount(chain, addr, denom)
190+
s.Require().NoError(err)
191+
}
192+
193+
afterBalance := s.getAccountBalance(chain, addr, denom)
194+
s.T().Log("address:", addr, "after balance: ", afterBalance, "before balance:", beforeBalance)
195+
196+
// Check that the balance has increased by at least the expected amount times the number of requests
197+
expectedIncrease := expCreditedAmt * float64(numRequests)
198+
actualIncrease := afterBalance - beforeBalance
199+
s.Require().GreaterOrEqual(actualIncrease, expectedIncrease,
200+
"Balance didn't increase as expected. Actual increase: %f, Expected increase: %f",
201+
actualIncrease, expectedIncrease)
202+
})
203+
}
204+
}

0 commit comments

Comments
 (0)