web/backend/secretapi/chain.go

130 lines
4.1 KiB
Go

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
}