Files

668 lines
16 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package contract
import (
_ "embed"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"reflect"
"strings"
"github.com/luxfi/cli/v2/pkg/ux"
"github.com/luxfi/cli/v2/sdk/evm"
sdkUtils "github.com/luxfi/cli/v2/sdk/utils"
luxWarp "github.com/luxfi/warp"
"github.com/luxfi/evm/v2/accounts/abi/bind"
"github.com/luxfi/geth/core/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
var ErrFailedReceiptStatus = fmt.Errorf("failed receipt status")
func removeSurroundingParenthesis(s string) (string, error) {
s = strings.TrimSpace(s)
if len(s) > 0 {
if string(s[0]) != "(" || string(s[len(s)-1]) != ")" {
return "", fmt.Errorf("expected esp %q to be surrounded by parenthesis", s)
}
s = s[1 : len(s)-1]
}
return s, nil
}
func removeSurroundingBrackets(s string) (string, error) {
s = strings.TrimSpace(s)
if len(s) > 0 {
if string(s[0]) != "[" || string(s[len(s)-1]) != "]" {
return "", fmt.Errorf("expected esp %q to be surrounded by parenthesis", s)
}
s = s[1 : len(s)-1]
}
return s, nil
}
func getWords(s string) []string {
words := []string{}
word := ""
parenthesisCount := 0
insideBrackets := false
for _, rune := range s {
c := string(rune)
if parenthesisCount > 0 {
word += c
if c == "(" {
parenthesisCount++
}
if c == ")" {
parenthesisCount--
if parenthesisCount == 0 {
words = append(words, word)
word = ""
}
}
continue
}
if insideBrackets {
word += c
if c == "]" {
words = append(words, word)
word = ""
insideBrackets = false
}
continue
}
if c == " " || c == "," || c == "(" || c == "[" {
if word != "" {
words = append(words, word)
word = ""
}
}
if c == " " || c == "," {
continue
}
if c == "(" {
parenthesisCount++
}
if c == "[" {
insideBrackets = true
}
word += c
}
if word != "" {
words = append(words, word)
}
return words
}
func getMap(
types []string,
params interface{},
) ([]map[string]interface{}, error) {
r := []map[string]interface{}{}
for i, t := range types {
var (
param interface{}
name string
structName string
)
rt := reflect.ValueOf(params)
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
if rt.Kind() == reflect.Slice {
if rt.Len() != len(types) {
if rt.Len() == 1 {
return getMap(types, rt.Index(0).Interface())
} else {
return nil, fmt.Errorf(
"inconsistency in slice len between method esp %q and given params %#v: expected %d got %d",
types,
params,
len(types),
rt.Len(),
)
}
}
param = rt.Index(i).Interface()
} else if rt.Kind() == reflect.Struct {
if rt.NumField() < len(types) {
return nil, fmt.Errorf(
"inconsistency in struct len between method esp %q and given params %#v: expected %d got %d",
types,
params,
len(types),
rt.NumField(),
)
}
name = rt.Type().Field(i).Name
structName = rt.Type().Field(i).Type.Name()
param = rt.Field(i).Interface()
}
m := map[string]interface{}{}
switch {
case string(t[0]) == "(":
// struct type
var err error
t, err = removeSurroundingParenthesis(t)
if err != nil {
return nil, err
}
m["components"], err = getMap(getWords(t), param)
if err != nil {
return nil, err
}
if structName != "" {
m["internalType"] = "struct " + structName
} else {
m["internalType"] = "tuple"
}
m["type"] = "tuple"
m["name"] = name
case string(t[0]) == "[":
var err error
t, err = removeSurroundingBrackets(t)
if err != nil {
return nil, err
}
if string(t[0]) == "(" {
t, err = removeSurroundingParenthesis(t)
if err != nil {
return nil, err
}
rt := reflect.ValueOf(param)
if rt.Kind() != reflect.Slice {
return nil, fmt.Errorf("expected param for field %d of esp %q to be an slice", i, types)
}
param = reflect.Zero(rt.Type().Elem()).Interface()
structName = rt.Type().Elem().Name()
m["components"], err = getMap(getWords(t), param)
if err != nil {
return nil, err
}
if structName != "" {
m["internalType"] = "struct " + structName + "[]"
} else {
m["internalType"] = "tuple[]"
}
m["type"] = "tuple[]"
m["name"] = name
} else {
m["internalType"] = fmt.Sprintf("%s[]", t)
m["type"] = fmt.Sprintf("%s[]", t)
m["name"] = name
}
default:
m["internalType"] = t
m["type"] = t
m["name"] = name
}
r = append(r, m)
}
return r, nil
}
func ParseSpec(
esp string,
indexedFields []int,
constructor bool,
event bool,
paid bool,
view bool,
params ...interface{},
) (string, string, error) {
index := strings.Index(esp, "(")
if index == -1 {
return esp, "", nil
}
name := esp[:index]
types := esp[index:]
inputs := ""
outputs := ""
index = strings.Index(types, "->")
if index == -1 {
inputs = types
} else {
inputs = types[:index]
outputs = types[index+2:]
}
var err error
inputs, err = removeSurroundingParenthesis(inputs)
if err != nil {
return "", "", err
}
outputs, err = removeSurroundingParenthesis(outputs)
if err != nil {
return "", "", err
}
inputTypes := getWords(inputs)
outputTypes := getWords(outputs)
inputsMaps, err := getMap(inputTypes, params)
if err != nil {
return "", "", err
}
outputsMaps, err := getMap(outputTypes, nil)
if err != nil {
return "", "", err
}
if event {
for i := range inputsMaps {
if sdkUtils.Belongs(indexedFields, i) {
inputsMaps[i]["indexed"] = true
}
}
}
abiMap := []map[string]interface{}{
{
"inputs": inputsMaps,
},
}
switch {
case paid:
abiMap[0]["stateMutability"] = "payable"
case view:
abiMap[0]["stateMutability"] = "view"
default:
abiMap[0]["stateMutability"] = "nonpayable"
}
switch {
case constructor:
abiMap[0]["type"] = "constructor"
case event:
abiMap[0]["type"] = "event"
abiMap[0]["name"] = name
delete(abiMap[0], "stateMutability")
default:
abiMap[0]["type"] = "function"
abiMap[0]["outputs"] = outputsMaps
abiMap[0]["name"] = name
}
abiBytes, err := json.MarshalIndent(abiMap, "", " ")
if err != nil {
return "", "", err
}
return name, string(abiBytes), nil
}
func idempotentSigner(
_ common.Address,
tx *types.Transaction,
) (*types.Transaction, error) {
return tx, nil
}
// get method name and types from [methodsSpec], then call it
// at the smart contract [contractAddress] with the given [params].
// also send [payment] tokens to it
func TxToMethod(
rpcURL string,
generateRawTxOnly bool,
from common.Address,
privateKey string,
contractAddress common.Address,
payment *big.Int,
description string,
errorSignatureToError map[string]error,
methodSpec string,
params ...interface{},
) (*types.Transaction, *types.Receipt, error) {
if privateKey == "" && from == (common.Address{}) {
return nil, nil, fmt.Errorf("from address and private key can't be both empty at TxToMethod")
}
if !generateRawTxOnly && privateKey == "" {
return nil, nil, fmt.Errorf("from private key must be defined to be able to sign the tx at TxToMethod")
}
methodName, methodABI, err := ParseSpec(methodSpec, nil, false, false, payment != nil, false, params...)
if err != nil {
return nil, nil, err
}
metadata := &bind.MetaData{
ABI: methodABI,
}
abi, err := metadata.GetAbi()
if err != nil {
return nil, nil, err
}
client, err := evm.GetClient(rpcURL)
if err != nil {
return nil, nil, err
}
defer client.Close()
contract := bind.NewBoundContract(contractAddress, *abi, client.EthClient, client.EthClient, client.EthClient)
var txOpts *bind.TransactOpts
if generateRawTxOnly {
txOpts = &bind.TransactOpts{
From: from,
Signer: idempotentSigner,
NoSend: true,
}
} else {
txOpts, err = client.GetTxOptsWithSigner(privateKey)
if err != nil {
return nil, nil, err
}
}
txOpts.Value = payment
tx, err := contract.Transact(txOpts, methodName, params...)
if err != nil {
trace, traceCallErr := DebugTraceCall(
rpcURL,
from,
privateKey,
contractAddress,
payment,
methodSpec,
params...,
)
if traceCallErr != nil {
ux.Logger.PrintToUser("Could not get debug trace for %s error on %s: %s", description, rpcURL, traceCallErr)
ux.Logger.PrintToUser("Verify --debug flag value when calling 'blockchain create'")
return tx, nil, err
}
if errorFromSignature, err := evm.GetErrorFromTrace(trace, errorSignatureToError); errorFromSignature != nil {
return tx, nil, errorFromSignature
} else {
ux.Logger.RedXToUser("failed to match error selector on trace: %s", err)
ux.Logger.PrintToUser("error trace for %s error:", description)
ux.Logger.PrintToUser("%#v", trace)
}
return tx, nil, err
}
if generateRawTxOnly {
return tx, nil, nil
}
receipt, success, err := client.WaitForTransaction(tx)
if err != nil {
return tx, nil, err
} else if !success {
return handleFailedReceiptStatus(
rpcURL,
description,
errorSignatureToError,
tx,
receipt,
)
}
return tx, receipt, nil
}
// get method name and types from [methodsSpec], then call it
// at the smart contract [contractAddress] with the given [params].
// send [warpMessage] on the same call, whose signature is
// going to be verified previously to pass it to the method
// also send [payment] tokens to it
func TxToMethodWithWarpMessage(
rpcURL string,
generateRawTxOnly bool,
from common.Address,
privateKey string,
contractAddress common.Address,
warpMessage *luxWarp.Message,
payment *big.Int,
description string,
errorSignatureToError map[string]error,
methodSpec string,
params ...interface{},
) (*types.Transaction, *types.Receipt, error) {
if privateKey == "" && from == (common.Address{}) {
return nil, nil, fmt.Errorf("from address and private key can't be both empty at TxToMethodWithWarpMessage")
}
if !generateRawTxOnly && privateKey == "" {
return nil, nil, fmt.Errorf("from private key must be defined to be able to sign the tx at TxToMethodWithWarpMessage")
}
methodName, methodABI, err := ParseSpec(methodSpec, nil, false, false, false, false, params...)
if err != nil {
return nil, nil, err
}
metadata := &bind.MetaData{
ABI: methodABI,
}
abi, err := metadata.GetAbi()
if err != nil {
return nil, nil, err
}
callData, err := abi.Pack(methodName, params...)
if err != nil {
return nil, nil, err
}
client, err := evm.GetClient(rpcURL)
if err != nil {
return nil, nil, err
}
defer client.Close()
tx, err := client.TransactWithWarpMessage(
from,
privateKey,
warpMessage,
contractAddress,
callData,
payment,
generateRawTxOnly,
)
if err != nil {
return nil, nil, err
}
if generateRawTxOnly {
return tx, nil, nil
}
if err := client.SendTransaction(tx); err != nil {
return tx, nil, err
}
receipt, success, err := client.WaitForTransaction(tx)
if err != nil {
return tx, receipt, err
} else if !success {
return handleFailedReceiptStatus(
rpcURL,
description,
errorSignatureToError,
tx,
receipt,
)
}
return tx, receipt, nil
}
func printFailedReceiptStatusMessage(
rpcURL string,
description string,
tx *types.Transaction,
) {
ux.Logger.PrintToUser("Failed receipt status for %s error on %s, tx hash %s",
description,
rpcURL,
tx.Hash(),
)
}
func handleFailedReceiptStatus(
rpcURL string,
description string,
errorSignatureToError map[string]error,
tx *types.Transaction,
receipt *types.Receipt,
) (*types.Transaction, *types.Receipt, error) {
trace, err := evm.GetTxTrace(
rpcURL,
tx.Hash().String(),
)
if err != nil {
printFailedReceiptStatusMessage(rpcURL, description, tx)
ux.Logger.PrintToUser("Could not get debug trace: %s", err)
ux.Logger.PrintToUser("Verify --debug flag value when calling 'blockchain create'")
return tx, receipt, err
}
if errorFromSignature, err := evm.GetErrorFromTrace(trace, errorSignatureToError); errorFromSignature != nil {
return tx, receipt, errorFromSignature
} else {
printFailedReceiptStatusMessage(rpcURL, description, tx)
ux.Logger.RedXToUser("failed to match error selector on trace: %s", err)
ux.Logger.PrintToUser("error trace:")
ux.Logger.PrintToUser("%#v", trace)
}
return tx, receipt, ErrFailedReceiptStatus
}
func DebugTraceCall(
rpcURL string,
from common.Address,
privateKey string,
contractAddress common.Address,
payment *big.Int,
methodSpec string,
params ...interface{},
) (map[string]interface{}, error) {
methodName, methodABI, err := ParseSpec(methodSpec, nil, false, false, false, false, params...)
if err != nil {
return nil, err
}
metadata := &bind.MetaData{
ABI: methodABI,
}
abi, err := metadata.GetAbi()
if err != nil {
return nil, err
}
callData, err := abi.Pack(methodName, params...)
if err != nil {
return nil, err
}
client, err := evm.GetRawClient(rpcURL)
if err != nil {
return nil, err
}
defer client.Close()
if from == (common.Address{}) {
pk, err := crypto.HexToECDSA(privateKey)
if err != nil {
return nil, err
}
from = crypto.PubkeyToAddress(pk.PublicKey)
}
data := map[string]string{
"from": from.Hex(),
"to": contractAddress.Hex(),
"input": "0x" + hex.EncodeToString(callData),
}
if payment != nil {
hexBytes, _ := hexutil.Big(*payment).MarshalText()
data["value"] = string(hexBytes)
}
return client.DebugTraceCall(data)
}
func CallToMethod(
rpcURL string,
contractAddress common.Address,
methodSpec string,
params ...interface{},
) ([]interface{}, error) {
methodName, methodABI, err := ParseSpec(methodSpec, nil, false, false, false, true, params...)
if err != nil {
return nil, err
}
metadata := &bind.MetaData{
ABI: methodABI,
}
abi, err := metadata.GetAbi()
if err != nil {
return nil, err
}
client, err := evm.GetClient(rpcURL)
if err != nil {
return nil, err
}
defer client.Close()
contract := bind.NewBoundContract(contractAddress, *abi, client.EthClient, client.EthClient, client.EthClient)
var out []interface{}
err = contract.Call(&bind.CallOpts{}, &out, methodName, params...)
if err != nil {
return nil, err
}
return out, nil
}
func GetSmartContractCallResult[T any](methodName string, out []interface{}) (T, error) {
empty := new(T)
if len(out) == 0 {
return *empty, fmt.Errorf("error at %s call: no return value", methodName)
}
if len(out) != 1 {
return *empty, fmt.Errorf("error at %s call: expected 1 return value, got %d", methodName, len(out))
}
received, typeIsOk := out[0].(T)
if !typeIsOk {
return *empty, fmt.Errorf("error at %s call, expected %T, got %T", methodName, *empty, out[0])
}
return received, nil
}
func DeployContract(
rpcURL string,
privateKey string,
binBytes []byte,
methodSpec string,
params ...interface{},
) (common.Address, error) {
_, methodABI, err := ParseSpec(methodSpec, nil, true, false, false, false, params...)
if err != nil {
return common.Address{}, err
}
metadata := &bind.MetaData{
ABI: methodABI,
Bin: string(binBytes),
}
abi, err := metadata.GetAbi()
if err != nil {
return common.Address{}, err
}
bin := common.FromHex(metadata.Bin)
if len(bin) == 0 {
return common.Address{}, fmt.Errorf("failure on given binary for smart contract: zero len")
}
client, err := evm.GetClient(rpcURL)
if err != nil {
return common.Address{}, err
}
defer client.Close()
txOpts, err := client.GetTxOptsWithSigner(privateKey)
if err != nil {
return common.Address{}, err
}
address, tx, _, err := bind.DeployContract(txOpts, *abi, bin, client.EthClient, params...)
if err != nil {
return common.Address{}, err
}
if _, success, err := client.WaitForTransaction(tx); err != nil {
return common.Address{}, err
} else if !success {
return common.Address{}, ErrFailedReceiptStatus
}
return address, nil
}
func UnpackLog(
eventSpec string,
indexedFields []int,
log types.Log,
event interface{},
) error {
eventName, eventABI, err := ParseSpec(eventSpec, indexedFields, false, true, false, false, event)
if err != nil {
return err
}
metadata := &bind.MetaData{
ABI: eventABI,
}
abi, err := metadata.GetAbi()
if err != nil {
return err
}
contract := bind.NewBoundContract(common.Address{}, *abi, nil, nil, nil)
return contract.UnpackLog(event, eventName, log)
}