package main import ( "encoding/hex" "fmt" "strings" "time" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" ethmath "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" ) func buildTypedData(cfg Config, rec designationRecord) apitypes.TypedData { return apitypes.TypedData{ Types: apitypes.Types{ "EIP712Domain": []apitypes.Type{ {Name: "name", Type: "string"}, {Name: "version", Type: "string"}, {Name: "chainId", Type: "uint256"}, {Name: "verifyingContract", Type: "address"}, }, "DesignationIntent": []apitypes.Type{ {Name: "designationCode", Type: "string"}, {Name: "designationToken", Type: "string"}, {Name: "nonce", Type: "string"}, {Name: "issuedAt", Type: "string"}, {Name: "origin", Type: "string"}, }, }, PrimaryType: "DesignationIntent", Domain: apitypes.TypedDataDomain{ Name: cfg.DomainName, Version: "1", ChainId: mathHexOrDecimal256(cfg.ChainID), VerifyingContract: cfg.VerifyingContract, }, Message: apitypes.TypedDataMessage{ "designationCode": rec.Code, "designationToken": rec.DisplayToken, "nonce": rec.Nonce, "issuedAt": rec.IssuedAt.UTC().Format(time.RFC3339Nano), "origin": rec.Origin, }, } } func recoverSignerAddress(typedData apitypes.TypedData, signatureHex string) (string, error) { dataHash, _, err := apitypes.TypedDataAndHash(typedData) if err != nil { return "", fmt.Errorf("typed data hash: %w", err) } signatureHex = strings.TrimPrefix(strings.TrimSpace(signatureHex), "0x") sig, err := hex.DecodeString(signatureHex) if err != nil { return "", fmt.Errorf("decode signature: %w", err) } if len(sig) != 65 { return "", fmt.Errorf("invalid signature length: %d", len(sig)) } if sig[64] >= 27 { sig[64] -= 27 } pubKey, err := crypto.SigToPub(dataHash, sig) if err != nil { return "", fmt.Errorf("recover pubkey: %w", err) } return strings.ToLower(crypto.PubkeyToAddress(*pubKey).Hex()), nil } func normalizeAddress(address string) (string, error) { address = strings.TrimSpace(strings.ToLower(address)) if !common.IsHexAddress(address) { return "", fmt.Errorf("invalid wallet address") } return strings.ToLower(common.HexToAddress(address).Hex()), nil } func payerProofMessage(designationCode, ownerWallet, payerWallet string, chainID int64) string { return fmt.Sprintf( "EDUT-PAYER-AUTH:%s:%s:%s:%d", strings.TrimSpace(designationCode), strings.ToLower(strings.TrimSpace(ownerWallet)), strings.ToLower(strings.TrimSpace(payerWallet)), chainID, ) } func recoverPersonalSignAddress(message string, signatureHex string) (string, error) { signatureHex = strings.TrimPrefix(strings.TrimSpace(signatureHex), "0x") sig, err := hex.DecodeString(signatureHex) if err != nil { return "", fmt.Errorf("decode signature: %w", err) } if len(sig) != 65 { return "", fmt.Errorf("invalid signature length: %d", len(sig)) } if sig[64] >= 27 { sig[64] -= 27 } hash := accounts.TextHash([]byte(message)) pubKey, err := crypto.SigToPub(hash, sig) if err != nil { return "", fmt.Errorf("recover pubkey: %w", err) } return strings.ToLower(crypto.PubkeyToAddress(*pubKey).Hex()), nil } func verifyDistinctPayerProof(designationCode, ownerWallet, payerWallet string, chainID int64, signatureHex string) error { msg := payerProofMessage(designationCode, ownerWallet, payerWallet, chainID) recovered, err := recoverPersonalSignAddress(msg, signatureHex) if err != nil { return err } owner, err := normalizeAddress(ownerWallet) if err != nil { return err } if recovered != owner { return fmt.Errorf("ownership proof signer mismatch") } return nil } func mathHexOrDecimal256(v int64) *ethmath.HexOrDecimal256 { out := ethmath.NewHexOrDecimal256(v) return out }