201 lines
6.7 KiB
Go
201 lines
6.7 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/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)))
|
|
}
|