Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

faucet postcheck #549

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion starship/faucet/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,23 @@ func (a *Account) String() string {
return fmt.Sprintf("name: %s, addr: %s", a.Name, a.Address)
}

// queryTx queries the tx based on the txhash
func (a *Account) queryTx(txhash string) (map[string]interface{}, error) {
cmdStr := fmt.Sprintf("%s q tx %s --output=json", a.config.ChainBinary, txhash)
output, err := runCommand(cmdStr)
if err != nil {
return nil, err
}

txMap := map[string]interface{}{}
err = json.Unmarshal(output, &txMap)
if err != nil {
return nil, err
}

return txMap, nil
}

// sendTokens performs chain binary send txn from account
func (a *Account) sendTokens(address string, denom string, amount string) error {
ok := a.mu.TryLock()
Expand All @@ -284,7 +301,7 @@ func (a *Account) sendTokens(address string, denom string, amount string) error
}
defer a.mu.Unlock()

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)
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)
cmdStr := fmt.Sprintf("%s tx bank send %s %s %s%s %s", a.config.ChainBinary, a.Address, address, amount, denom, args)
output, err := runCommand(cmdStr)
if err != nil {
Expand All @@ -293,6 +310,30 @@ func (a *Account) sendTokens(address string, denom string, amount string) error
}
a.logger.Info("ran cmd to send tokens", zap.String("cmd", cmdStr), zap.String("stdout", string(output)))

// Find the JSON line and extract the txhash using a regular expression
txhash, err := extractTxHash(string(output))
if err != nil {
a.logger.Error("failed to extract txhash", zap.Error(err))
return err
}

a.logger.Debug("send tokens txhash", zap.String("txhash", txhash))

// query tx to check if the tx was successful
txMap, err := a.queryTx(txhash)
if err != nil {
return err
}

// check if the tx has the event
err = hasEvent(txMap, "transfer")
if err != nil {
a.logger.Error("transfer event not found in tx",
zap.String("txhash", txhash),
zap.Any("txMap", txMap))
return err
}

return nil
}

Expand Down
67 changes: 62 additions & 5 deletions starship/faucet/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package main

import (
"context"
"fmt"
"math/big"
"time"

"go.uber.org/zap"
"google.golang.org/protobuf/types/known/emptypb"

pb "github.com/cosmology-tech/starship/faucet/faucet"
Expand Down Expand Up @@ -37,11 +41,64 @@ func (a *AppServer) Status(ctx context.Context, _ *emptypb.Empty) (*pb.State, er
return state, nil
}

func (a *AppServer) getBalance(address, denom string) (*big.Int, error) {
account := &Account{config: a.config, logger: a.logger, Address: address}
coin, err := account.GetBalanceByDenom(denom)
if err != nil {
// Log the error, but don't return it
a.logger.Debug("Error getting balance, assuming new account", zap.Error(err))
return new(big.Int), nil // Return 0 balance
}
balance, ok := new(big.Int).SetString(coin.Amount, 10)
if !ok {
return nil, fmt.Errorf("failed to parse balance")
}
return balance, nil
}

func (a *AppServer) Credit(ctx context.Context, requestCredit *pb.RequestCredit) (*pb.ResponseCredit, error) {
err := a.distributor.SendTokens(requestCredit.GetAddress(), requestCredit.GetDenom())
if err != nil {
return nil, err
}
// Get initial balance before sending tokens
initialBalance, err := a.getBalance(requestCredit.GetAddress(), requestCredit.GetDenom())
if err != nil {
return nil, fmt.Errorf("failed to get initial balance: %v", err)
}

err = a.distributor.SendTokens(requestCredit.GetAddress(), requestCredit.GetDenom())
if err != nil {
return nil, err
}

// Check balance after transfer
confirmed, err := a.confirmBalanceUpdate(requestCredit.GetAddress(), requestCredit.GetDenom(), initialBalance)
if err != nil {
return &pb.ResponseCredit{Status: fmt.Sprintf("error: %v", err)}, err
}
if !confirmed {
return &pb.ResponseCredit{Status: "error: failed to confirm balance update (timeout)"}, nil
}

return &pb.ResponseCredit{Status: "ok"}, nil
}

func (a *AppServer) confirmBalanceUpdate(address, denom string, initialBalance *big.Int) (bool, error) {
expectedIncrease, ok := new(big.Int).SetString(a.distributor.CreditCoins.GetDenomAmount(denom), 10)
if !ok {
return false, fmt.Errorf("failed to parse expected amount")
}

expectedFinalBalance := new(big.Int).Add(initialBalance, expectedIncrease)

return &pb.ResponseCredit{Status: "ok"}, nil
for i := 0; i < 3; i++ { // Try 3 times with 5-second intervals
currentBalance, err := a.getBalance(address, denom)
if err != nil {
return false, err
}
if currentBalance.Cmp(expectedFinalBalance) >= 0 {
return true, nil
}
if i < 2 {
time.Sleep(5 * time.Second)
}
}
return false, nil
}
51 changes: 51 additions & 0 deletions starship/faucet/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/exec"
"regexp"
)

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

return outb.Bytes(), nil
}

// extractTxHash uses a regular expression to extract the txhash from the output
func extractTxHash(output string) (string, error) {
// Regular expression to match the txhash value in the JSON output
re := regexp.MustCompile(`"txhash":"([0-9A-Fa-f]{64})"`)
match := re.FindStringSubmatch(output)
if len(match) < 2 {
return "", fmt.Errorf("txhash not found in output")
}
return match[1], nil
}

// hasEvent checks for a specific event type in the transaction results.
// It first tries to fetch the events either from txMap["events"] or from txMap["logs"] and then checks for the event type.
func hasEvent(txResults map[string]interface{}, eventType string) error {
var events []interface{}

// Attempt to fetch events directly from txMap["events"]
if directEvents, ok := txResults["events"].([]interface{}); ok && len(directEvents) > 0 {
events = directEvents
} else if logs, ok := txResults["logs"].([]interface{}); ok && len(logs) > 0 {
// If no direct events, attempt to fetch events from logs
for _, log := range logs {
logMap, ok := log.(map[string]interface{})
if !ok {
continue
}
if logEvents, ok := logMap["events"].([]interface{}); ok && len(logEvents) > 0 {
events = append(events, logEvents...)
}
}
}

// If events are found, check for the specific eventType
if len(events) > 0 {
for _, event := range events {
eventMap, ok := event.(map[string]interface{})
if !ok {
continue
}
if eventMap["type"] == eventType {
return nil
}
}
return fmt.Errorf("event %s not found in transaction events", eventType)
}

// If no events were found in either place
return fmt.Errorf("no events found in transaction")
}
76 changes: 62 additions & 14 deletions starship/tests/e2e/faucet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"bytes"
"encoding/json"
"fmt"
pb "github.com/cosmology-tech/starship/registry/registry"
"net/http"
urlpkg "net/url"
"strconv"
"time"

pb "github.com/cosmology-tech/starship/registry/registry"
)

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

func (s *TestSuite) creditAccount(chain *Chain, addr, denom string) error {
body := map[string]string{
"denom": denom,
"address": addr,
}
postBody, err := json.Marshal(body)
if err != nil {
return err
}
resp, err := http.Post(
fmt.Sprintf("http://0.0.0.0:%d/credit", chain.Ports.Faucet),
"application/json",
bytes.NewBuffer(postBody))
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return nil
}

func (s *TestSuite) TestFaucet_Credit() {
s.T().Log("running test for /credit endpoint for faucet")

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

body := map[string]string{
"denom": denom,
"address": addr,
}
postBody, err := json.Marshal(body)
s.Require().NoError(err)
resp, err := http.Post(
fmt.Sprintf("http://0.0.0.0:%d/credit", chain.Ports.Faucet),
"application/json",
bytes.NewBuffer(postBody))
err := s.creditAccount(chain, addr, denom)
s.Require().NoError(err)
s.Require().Equal(200, resp.StatusCode)

time.Sleep(4 * time.Second)
afterBalance := s.getAccountBalance(chain, addr, denom)
s.T().Log("address:", addr, "after balance: ", afterBalance, "before balance:", beforeBalance)
// note sometimes expected difference is 9x expected value (bug due to using holder address for test)
Expand All @@ -154,3 +165,40 @@ func (s *TestSuite) TestFaucet_Credit() {
})
}
}

func (s *TestSuite) TestFaucet_Credit_MultipleRequests() {
s.T().Log("running test for multiple requests to /credit endpoint for faucet")

// expected amount to be credited via faucet
expCreditedAmt := float64(10000000000)

for _, chain := range s.config.Chains {
s.Run(fmt.Sprintf("multiple faucet requests test for: %s", chain.ID), func() {
if chain.Ports.Faucet == 0 {
s.T().Skip("faucet not exposed via ports")
}

// fetch denom and address from an account on chain
denom := s.getChainDenoms(chain)
addr := getAddressFromType(chain.Name)
beforeBalance := s.getAccountBalance(chain, addr, denom)

// Send multiple requests
numRequests := 3
for i := 0; i < numRequests; i++ {
err := s.creditAccount(chain, addr, denom)
s.Require().NoError(err)
}

afterBalance := s.getAccountBalance(chain, addr, denom)
s.T().Log("address:", addr, "after balance: ", afterBalance, "before balance:", beforeBalance)

// Check that the balance has increased by at least the expected amount times the number of requests
expectedIncrease := expCreditedAmt * float64(numRequests)
actualIncrease := afterBalance - beforeBalance
s.Require().GreaterOrEqual(actualIncrease, expectedIncrease,
"Balance didn't increase as expected. Actual increase: %f, Expected increase: %f",
actualIncrease, expectedIncrease)
})
}
}
Loading