package main import ( "context" "fmt" "math/big" "strings" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" ) const membershipABI = `[{"inputs":[{"internalType":"address","name":"recipient","type":"address"}],"name":"mintMembership","outputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"wallet","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountPaid","type":"uint256"},{"indexed":false,"internalType":"address","name":"currency","type":"address"}],"name":"MembershipMinted","type":"event"}]` func encodeMintMembershipCalldata(recipient string) (string, error) { parsed, err := abi.JSON(strings.NewReader(membershipABI)) if err != nil { return "", fmt.Errorf("parse membership abi: %w", err) } address := common.HexToAddress(recipient) data, err := parsed.Pack("mintMembership", address) if err != nil { return "", fmt.Errorf("pack mint calldata: %w", err) } return "0x" + common.Bytes2Hex(data), nil } func verifyMintedOnChain(ctx context.Context, cfg Config, txHash string, expectedWallet string) error { if strings.TrimSpace(cfg.ChainRPCURL) == "" { return nil } client, err := ethclient.DialContext(ctx, cfg.ChainRPCURL) if err != nil { return fmt.Errorf("dial chain rpc: %w", err) } defer client.Close() hash := common.HexToHash(txHash) receipt, err := client.TransactionReceipt(ctx, hash) if err != nil { return fmt.Errorf("read tx receipt: %w", err) } if receipt == nil { return fmt.Errorf("tx receipt not found") } if receipt.Status != types.ReceiptStatusSuccessful { return fmt.Errorf("tx failed status=%d", receipt.Status) } parsed, err := abi.JSON(strings.NewReader(membershipABI)) if err != nil { return fmt.Errorf("parse membership abi: %w", err) } mintedEvent := parsed.Events["MembershipMinted"] expectedWallet = strings.ToLower(common.HexToAddress(expectedWallet).Hex()) for _, lg := range receipt.Logs { if strings.ToLower(lg.Address.Hex()) != strings.ToLower(common.HexToAddress(cfg.MembershipContract).Hex()) { continue } if len(lg.Topics) == 0 || lg.Topics[0] != mintedEvent.ID { continue } if len(lg.Topics) < 2 { continue } wallet := common.HexToAddress(lg.Topics[1].Hex()).Hex() if strings.ToLower(wallet) == expectedWallet { return nil } } return fmt.Errorf("membership mint event not found for wallet") } func verifyTransactionSenderOnChain(ctx context.Context, cfg Config, txHash string, expectedSender string) error { if strings.TrimSpace(cfg.ChainRPCURL) == "" { return nil } expectedSender = strings.TrimSpace(expectedSender) if expectedSender == "" { return fmt.Errorf("expected sender missing") } client, err := ethclient.DialContext(ctx, cfg.ChainRPCURL) if err != nil { return fmt.Errorf("dial chain rpc: %w", err) } defer client.Close() hash := common.HexToHash(txHash) receipt, err := client.TransactionReceipt(ctx, hash) if err != nil { return fmt.Errorf("read tx receipt: %w", err) } if receipt == nil { return fmt.Errorf("tx receipt not found") } if receipt.Status != types.ReceiptStatusSuccessful { return fmt.Errorf("tx failed status=%d", receipt.Status) } tx, pending, err := client.TransactionByHash(ctx, hash) if err != nil { return fmt.Errorf("read tx body: %w", err) } if pending { return fmt.Errorf("tx pending") } chainID := tx.ChainId() if chainID == nil || chainID.Sign() <= 0 { chainID = big.NewInt(cfg.ChainID) } signer := types.LatestSignerForChainID(chainID) from, err := types.Sender(signer, tx) if err != nil { return fmt.Errorf("resolve tx sender: %w", err) } gotSender := strings.ToLower(from.Hex()) wantSender := strings.ToLower(common.HexToAddress(expectedSender).Hex()) if gotSender != wantSender { return fmt.Errorf("tx sender mismatch got=%s want=%s", gotSender, wantSender) } return nil }