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/crypto" "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"}]` const entitlementABI = `[{"inputs":[{"internalType":"string","name":"offerId","type":"string"},{"internalType":"address","name":"ownerWallet","type":"address"},{"internalType":"bytes32","name":"orgRootKey","type":"bytes32"},{"internalType":"bytes32","name":"workspaceKey","type":"bytes32"}],"name":"purchaseEntitlement","outputs":[{"internalType":"uint256","name":"entitlementId","type":"uint256"}],"stateMutability":"payable","type":"function"}]` 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 { return verifyTransactionCallOnChain(ctx, cfg, txHash, expectedSender, "", "", "") } func verifyTransactionCallOnChain( ctx context.Context, cfg Config, txHash string, expectedSender string, expectedTo string, expectedData string, expectedValueHex string, ) error { if strings.TrimSpace(cfg.ChainRPCURL) == "" { return nil } expectedSender = strings.TrimSpace(expectedSender) expectedTo = strings.TrimSpace(expectedTo) expectedData = strings.ToLower(strings.TrimSpace(expectedData)) expectedValueHex = strings.ToLower(strings.TrimSpace(expectedValueHex)) if expectedSender == "" && expectedTo == "" && expectedData == "" && expectedValueHex == "" { return fmt.Errorf("no expected tx parameters provided") } 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) } if expectedSender != "" { 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) } } if expectedTo != "" { if tx.To() == nil { return fmt.Errorf("tx target missing") } gotTo := strings.ToLower(tx.To().Hex()) wantTo := strings.ToLower(common.HexToAddress(expectedTo).Hex()) if gotTo != wantTo { return fmt.Errorf("tx target mismatch got=%s want=%s", gotTo, wantTo) } } if expectedData != "" { gotData := "0x" + common.Bytes2Hex(tx.Data()) if strings.ToLower(gotData) != expectedData { return fmt.Errorf("tx calldata mismatch") } } if expectedValueHex != "" { wantValue := new(big.Int) if _, ok := wantValue.SetString(strings.TrimPrefix(expectedValueHex, "0x"), 16); !ok { return fmt.Errorf("invalid expected tx value") } if tx.Value().Cmp(wantValue) != 0 { return fmt.Errorf("tx value mismatch got=%s want=%s", tx.Value().String(), wantValue.String()) } } return nil } func encodePurchaseEntitlementCalldata(offerID, ownerWallet, orgRootID, workspaceID string) (string, error) { parsed, err := abi.JSON(strings.NewReader(entitlementABI)) if err != nil { return "", fmt.Errorf("parse entitlement abi: %w", err) } ownerAddress := common.HexToAddress(ownerWallet) orgRootKey := hashIdentifierKey(orgRootID) workspaceKey := hashIdentifierKey(workspaceID) data, err := parsed.Pack("purchaseEntitlement", strings.TrimSpace(offerID), ownerAddress, orgRootKey, workspaceKey) if err != nil { return "", fmt.Errorf("pack entitlement calldata: %w", err) } return "0x" + common.Bytes2Hex(data), nil } func hashIdentifierKey(value string) common.Hash { value = strings.TrimSpace(value) if value == "" { return common.Hash{} } return common.BytesToHash(crypto.Keccak256([]byte(value))) }