// Package contract provides smart contract deployment and interaction. package contract import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "sync/atomic" "time" ) const DefaultEndpoint = "https://contract.synor.cc/api/v1" // Config is the contract client configuration. type Config struct { APIKey string Endpoint string Timeout time.Duration Retries int Debug bool DefaultGasLimit string DefaultGasPrice string } // AbiEntryType is the type of ABI entry. type AbiEntryType string const ( AbiFunction AbiEntryType = "function" AbiConstructor AbiEntryType = "constructor" AbiEvent AbiEntryType = "event" AbiError AbiEntryType = "error" AbiFallback AbiEntryType = "fallback" AbiReceive AbiEntryType = "receive" ) // StateMutability represents function state mutability. type StateMutability string const ( StatePure StateMutability = "pure" StateView StateMutability = "view" StateNonpayable StateMutability = "nonpayable" StatePayable StateMutability = "payable" ) // TransactionStatus represents transaction status. type TransactionStatus string const ( StatusSuccess TransactionStatus = "success" StatusReverted TransactionStatus = "reverted" ) // VerificationStatus represents verification status. type VerificationStatus string const ( VerificationVerified VerificationStatus = "verified" VerificationPending VerificationStatus = "pending" VerificationFailed VerificationStatus = "failed" ) // AbiParameter represents an ABI parameter. type AbiParameter struct { Name string `json:"name"` Type string `json:"type"` Indexed bool `json:"indexed,omitempty"` Components []AbiParameter `json:"components,omitempty"` InternalType string `json:"internalType,omitempty"` } // AbiEntry represents an ABI entry. type AbiEntry struct { Type AbiEntryType `json:"type"` Name string `json:"name,omitempty"` Inputs []AbiParameter `json:"inputs,omitempty"` Outputs []AbiParameter `json:"outputs,omitempty"` StateMutability StateMutability `json:"stateMutability,omitempty"` Anonymous bool `json:"anonymous,omitempty"` } // Abi is a contract ABI. type Abi []AbiEntry // DeploymentResult is the result of a contract deployment. type DeploymentResult struct { Address string `json:"address"` TransactionHash string `json:"transactionHash"` BlockNumber int64 `json:"blockNumber"` GasUsed string `json:"gasUsed"` EffectiveGasPrice string `json:"effectiveGasPrice"` } // EventLog is a raw event log. type EventLog struct { LogIndex int `json:"logIndex"` Address string `json:"address"` Topics []string `json:"topics"` Data string `json:"data"` BlockNumber int64 `json:"blockNumber"` TransactionHash string `json:"transactionHash"` } // DecodedEvent is a decoded event. type DecodedEvent struct { Name string `json:"name"` Signature string `json:"signature"` Args map[string]interface{} `json:"args"` Log EventLog `json:"log"` } // TransactionResult is the result of a transaction. type TransactionResult struct { TransactionHash string `json:"transactionHash"` BlockNumber int64 `json:"blockNumber"` BlockHash string `json:"blockHash"` GasUsed string `json:"gasUsed"` EffectiveGasPrice string `json:"effectiveGasPrice"` Status TransactionStatus `json:"status"` Logs []EventLog `json:"logs"` ReturnValue interface{} `json:"returnValue,omitempty"` RevertReason string `json:"revertReason,omitempty"` } // ContractInterface is a parsed contract interface. type ContractInterface struct { Abi Abi Functions map[string]AbiEntry Events map[string]AbiEntry Errors map[string]AbiEntry } // GasEstimation is a gas estimation result. type GasEstimation struct { GasLimit string `json:"gasLimit"` GasPrice string `json:"gasPrice"` EstimatedCost string `json:"estimatedCost"` } // BytecodeMetadata contains bytecode metadata. type BytecodeMetadata struct { Compiler string `json:"compiler"` Language string `json:"language"` Sources []string `json:"sources"` } // BytecodeInfo contains contract bytecode information. type BytecodeInfo struct { Bytecode string `json:"bytecode"` DeployedBytecode string `json:"deployedBytecode,omitempty"` Abi Abi `json:"abi,omitempty"` Metadata *BytecodeMetadata `json:"metadata,omitempty"` } // VerificationResult is the result of contract verification. type VerificationResult struct { Status VerificationStatus `json:"status"` Message string `json:"message"` Abi Abi `json:"abi,omitempty"` } // MulticallRequest is a multicall request. type MulticallRequest struct { Address string `json:"address"` CallData string `json:"callData"` AllowFailure bool `json:"allowFailure,omitempty"` } // MulticallResult is a multicall result. type MulticallResult struct { Success bool `json:"success"` ReturnData string `json:"returnData"` Decoded interface{} `json:"decoded,omitempty"` } // Error is a contract error. type Error struct { Message string Code string StatusCode int } func (e *Error) Error() string { return e.Message } // Client is a Synor Contract client. type Client struct { config Config httpClient *http.Client closed atomic.Bool } // NewClient creates a new contract client. func NewClient(config Config) *Client { if config.Endpoint == "" { config.Endpoint = DefaultEndpoint } if config.Timeout == 0 { config.Timeout = 30 * time.Second } if config.Retries == 0 { config.Retries = 3 } return &Client{ config: config, httpClient: &http.Client{ Timeout: config.Timeout, }, } } // ==================== Deployment ==================== // DeployOptions are options for deploying a contract. type DeployOptions struct { Bytecode string Abi Abi Args []interface{} GasLimit string GasPrice string Value string Salt string } // Deploy deploys a smart contract. func (c *Client) Deploy(ctx context.Context, opts DeployOptions) (*DeploymentResult, error) { body := map[string]interface{}{ "bytecode": opts.Bytecode, } if opts.Abi != nil { body["abi"] = opts.Abi } if opts.Args != nil { body["args"] = opts.Args } gasLimit := opts.GasLimit if gasLimit == "" { gasLimit = c.config.DefaultGasLimit } if gasLimit != "" { body["gasLimit"] = gasLimit } gasPrice := opts.GasPrice if gasPrice == "" { gasPrice = c.config.DefaultGasPrice } if gasPrice != "" { body["gasPrice"] = gasPrice } if opts.Value != "" { body["value"] = opts.Value } if opts.Salt != "" { body["salt"] = opts.Salt } var resp struct { Deployment DeploymentResult `json:"deployment"` } if err := c.request(ctx, "POST", "/contracts/deploy", body, &resp); err != nil { return nil, err } return &resp.Deployment, nil } // PredictAddress predicts the CREATE2 deployment address. func (c *Client) PredictAddress(ctx context.Context, bytecode, salt string, constructorArgs []interface{}) (string, error) { body := map[string]interface{}{ "bytecode": bytecode, "salt": salt, } if constructorArgs != nil { body["constructorArgs"] = constructorArgs } var resp struct { Address string `json:"address"` } if err := c.request(ctx, "POST", "/contracts/predict-address", body, &resp); err != nil { return "", err } return resp.Address, nil } // ==================== Contract Interaction ==================== // CallOptions are options for calling a contract. type CallOptions struct { Address string Method string Args []interface{} Abi Abi BlockNumber interface{} } // Call calls a view/pure function. func (c *Client) Call(ctx context.Context, opts CallOptions) (interface{}, error) { body := map[string]interface{}{ "address": opts.Address, "method": opts.Method, "abi": opts.Abi, } if opts.Args != nil { body["args"] = opts.Args } if opts.BlockNumber != nil { body["blockNumber"] = opts.BlockNumber } else { body["blockNumber"] = "latest" } var resp struct { Result interface{} `json:"result"` } if err := c.request(ctx, "POST", "/contracts/call", body, &resp); err != nil { return nil, err } return resp.Result, nil } // SendOptions are options for sending a transaction. type SendOptions struct { Address string Method string Args []interface{} Abi Abi GasLimit string GasPrice string Value string } // Send sends a state-changing transaction. func (c *Client) Send(ctx context.Context, opts SendOptions) (*TransactionResult, error) { body := map[string]interface{}{ "address": opts.Address, "method": opts.Method, "abi": opts.Abi, } if opts.Args != nil { body["args"] = opts.Args } gasLimit := opts.GasLimit if gasLimit == "" { gasLimit = c.config.DefaultGasLimit } if gasLimit != "" { body["gasLimit"] = gasLimit } gasPrice := opts.GasPrice if gasPrice == "" { gasPrice = c.config.DefaultGasPrice } if gasPrice != "" { body["gasPrice"] = gasPrice } if opts.Value != "" { body["value"] = opts.Value } var resp struct { Transaction TransactionResult `json:"transaction"` } if err := c.request(ctx, "POST", "/contracts/send", body, &resp); err != nil { return nil, err } return &resp.Transaction, nil } // Multicall executes multiple calls in a single request. func (c *Client) Multicall(ctx context.Context, requests []MulticallRequest, abis map[string]Abi) ([]MulticallResult, error) { body := map[string]interface{}{ "calls": requests, } if abis != nil { body["abis"] = abis } var resp struct { Results []MulticallResult `json:"results"` } if err := c.request(ctx, "POST", "/contracts/multicall", body, &resp); err != nil { return nil, err } return resp.Results, nil } // ==================== Events ==================== // EventFilter is a filter for events. type EventFilter struct { Address string EventName string Abi Abi FromBlock interface{} ToBlock interface{} Filter map[string]interface{} } // GetEvents gets historical events. func (c *Client) GetEvents(ctx context.Context, filter EventFilter) ([]DecodedEvent, error) { body := map[string]interface{}{ "address": filter.Address, } if filter.Abi != nil { body["abi"] = filter.Abi } if filter.EventName != "" { body["eventName"] = filter.EventName } if filter.FromBlock != nil { body["fromBlock"] = filter.FromBlock } else { body["fromBlock"] = "earliest" } if filter.ToBlock != nil { body["toBlock"] = filter.ToBlock } else { body["toBlock"] = "latest" } if filter.Filter != nil { body["filter"] = filter.Filter } var resp struct { Events []DecodedEvent `json:"events"` } if err := c.request(ctx, "POST", "/contracts/events", body, &resp); err != nil { return nil, err } return resp.Events, nil } // GetLogs gets raw event logs. func (c *Client) GetLogs(ctx context.Context, filter EventFilter) ([]EventLog, error) { body := map[string]interface{}{ "address": filter.Address, } if filter.Abi != nil { body["abi"] = filter.Abi } if filter.EventName != "" { body["eventName"] = filter.EventName } if filter.FromBlock != nil { body["fromBlock"] = filter.FromBlock } else { body["fromBlock"] = "earliest" } if filter.ToBlock != nil { body["toBlock"] = filter.ToBlock } else { body["toBlock"] = "latest" } if filter.Filter != nil { body["filter"] = filter.Filter } var resp struct { Logs []EventLog `json:"logs"` } if err := c.request(ctx, "POST", "/contracts/logs", body, &resp); err != nil { return nil, err } return resp.Logs, nil } // DecodeLog decodes a raw event log. func (c *Client) DecodeLog(ctx context.Context, log EventLog, abi Abi) (*DecodedEvent, error) { body := map[string]interface{}{ "log": log, "abi": abi, } var resp struct { Event DecodedEvent `json:"event"` } if err := c.request(ctx, "POST", "/contracts/decode-log", body, &resp); err != nil { return nil, err } return &resp.Event, nil } // ==================== ABI Utilities ==================== // LoadAbi loads and parses a contract ABI. func (c *Client) LoadAbi(abi Abi) *ContractInterface { ci := &ContractInterface{ Abi: abi, Functions: make(map[string]AbiEntry), Events: make(map[string]AbiEntry), Errors: make(map[string]AbiEntry), } for _, entry := range abi { switch entry.Type { case AbiFunction: if entry.Name != "" { ci.Functions[entry.Name] = entry } case AbiEvent: if entry.Name != "" { ci.Events[entry.Name] = entry } case AbiError: if entry.Name != "" { ci.Errors[entry.Name] = entry } } } return ci } // EncodeCall encodes a function call. func (c *Client) EncodeCall(ctx context.Context, method string, args []interface{}, abi Abi) (string, error) { body := map[string]interface{}{ "method": method, "args": args, "abi": abi, } var resp struct { Data string `json:"data"` } if err := c.request(ctx, "POST", "/contracts/encode", body, &resp); err != nil { return "", err } return resp.Data, nil } // DecodeResult decodes function return data. func (c *Client) DecodeResult(ctx context.Context, method, data string, abi Abi) (interface{}, error) { body := map[string]interface{}{ "method": method, "data": data, "abi": abi, } var resp struct { Result interface{} `json:"result"` } if err := c.request(ctx, "POST", "/contracts/decode", body, &resp); err != nil { return nil, err } return resp.Result, nil } // GetFunctionSelector gets the function selector. func (c *Client) GetFunctionSelector(ctx context.Context, signature string) (string, error) { body := map[string]interface{}{ "signature": signature, } var resp struct { Selector string `json:"selector"` } if err := c.request(ctx, "POST", "/contracts/selector", body, &resp); err != nil { return "", err } return resp.Selector, nil } // ==================== Gas Estimation ==================== // EstimateGasOptions are options for gas estimation. type EstimateGasOptions struct { Address string Method string Args []interface{} Abi Abi Bytecode string Value string From string } // EstimateGas estimates gas for a contract call or deployment. func (c *Client) EstimateGas(ctx context.Context, opts EstimateGasOptions) (*GasEstimation, error) { body := map[string]interface{}{} if opts.Address != "" { body["address"] = opts.Address } if opts.Method != "" { body["method"] = opts.Method } if opts.Args != nil { body["args"] = opts.Args } if opts.Abi != nil { body["abi"] = opts.Abi } if opts.Bytecode != "" { body["bytecode"] = opts.Bytecode } if opts.Value != "" { body["value"] = opts.Value } if opts.From != "" { body["from"] = opts.From } var resp struct { Estimation GasEstimation `json:"estimation"` } if err := c.request(ctx, "POST", "/contracts/estimate-gas", body, &resp); err != nil { return nil, err } return &resp.Estimation, nil } // ==================== Contract Info ==================== // GetBytecode gets contract bytecode. func (c *Client) GetBytecode(ctx context.Context, address string) (*BytecodeInfo, error) { var info BytecodeInfo if err := c.request(ctx, "GET", fmt.Sprintf("/contracts/%s/bytecode", address), nil, &info); err != nil { return nil, err } return &info, nil } // IsContract checks if an address is a contract. func (c *Client) IsContract(ctx context.Context, address string) (bool, error) { var resp struct { IsContract bool `json:"isContract"` } if err := c.request(ctx, "GET", fmt.Sprintf("/contracts/%s/is-contract", address), nil, &resp); err != nil { return false, err } return resp.IsContract, nil } // ReadStorage reads a contract storage slot. func (c *Client) ReadStorage(ctx context.Context, address, slot string, blockNumber interface{}) (string, error) { body := map[string]interface{}{ "address": address, "slot": slot, } if blockNumber != nil { body["blockNumber"] = blockNumber } else { body["blockNumber"] = "latest" } var resp struct { Value string `json:"value"` } if err := c.request(ctx, "POST", "/contracts/storage", body, &resp); err != nil { return "", err } return resp.Value, nil } // ==================== Verification ==================== // VerifyContractOptions are options for contract verification. type VerifyContractOptions struct { Address string SourceCode string CompilerVersion string ConstructorArguments string Optimization bool OptimizationRuns int ContractName string } // VerifyContract submits a contract for verification. func (c *Client) VerifyContract(ctx context.Context, opts VerifyContractOptions) (*VerificationResult, error) { body := map[string]interface{}{ "address": opts.Address, "sourceCode": opts.SourceCode, "compilerVersion": opts.CompilerVersion, "optimization": opts.Optimization, "optimizationRuns": opts.OptimizationRuns, } if opts.ConstructorArguments != "" { body["constructorArguments"] = opts.ConstructorArguments } if opts.ContractName != "" { body["contractName"] = opts.ContractName } var result VerificationResult if err := c.request(ctx, "POST", "/contracts/verify", body, &result); err != nil { return nil, err } return &result, nil } // GetVerificationStatus gets verification status. func (c *Client) GetVerificationStatus(ctx context.Context, address string) (*VerificationResult, error) { var result VerificationResult if err := c.request(ctx, "GET", fmt.Sprintf("/contracts/%s/verification", address), nil, &result); err != nil { return nil, err } return &result, nil } // GetVerifiedAbi gets the verified contract ABI. func (c *Client) GetVerifiedAbi(ctx context.Context, address string) (Abi, error) { var resp struct { Abi Abi `json:"abi"` } if err := c.request(ctx, "GET", fmt.Sprintf("/contracts/%s/abi", address), nil, &resp); err != nil { return nil, err } return resp.Abi, nil } // ==================== Lifecycle ==================== // HealthCheck checks if the service is healthy. func (c *Client) HealthCheck(ctx context.Context) bool { var resp struct { Status string `json:"status"` } if err := c.request(ctx, "GET", "/health", nil, &resp); err != nil { return false } return resp.Status == "healthy" } // Close closes the client. func (c *Client) Close() { c.closed.Store(true) } // IsClosed returns whether the client is closed. func (c *Client) IsClosed() bool { return c.closed.Load() } // ==================== Private ==================== func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error { if c.closed.Load() { return &Error{Message: "client has been closed"} } var lastErr error for attempt := 0; attempt < c.config.Retries; attempt++ { err := c.doRequest(ctx, method, path, body, result) if err == nil { return nil } lastErr = err if attempt < c.config.Retries-1 { time.Sleep(time.Duration(1<= 400 { var errResp struct { Message string `json:"message"` Error string `json:"error"` Code string `json:"code"` } json.Unmarshal(respBody, &errResp) msg := errResp.Message if msg == "" { msg = errResp.Error } if msg == "" { msg = fmt.Sprintf("HTTP %d", resp.StatusCode) } return &Error{ Message: msg, Code: errResp.Code, StatusCode: resp.StatusCode, } } if result != nil { if err := json.Unmarshal(respBody, result); err != nil { return &Error{Message: fmt.Sprintf("failed to parse response: %v", err)} } } return nil }