Privacy SDK features: - Confidential transactions with Pedersen commitments - Bulletproof range proofs for value validation - Ring signatures for anonymous signing with key images - Stealth addresses for unlinkable payments - Blinding factor generation and value operations Contract SDK features: - Smart contract deployment (standard and CREATE2) - Call (view/pure) and Send (state-changing) operations - Event log filtering, subscription, and decoding - ABI encoding/decoding utilities - Gas estimation and contract verification - Multicall for batched operations - Storage slot reading Languages implemented: - JavaScript/TypeScript - Python (async with httpx) - Go - Rust (async with reqwest/tokio) - Java (async with OkHttp) - Kotlin (coroutines with Ktor) - Swift (async/await with URLSession) - Flutter/Dart - C (header-only interface) - C++ (header-only with std::future) - C#/.NET (async with HttpClient) - Ruby (Faraday HTTP client) All SDKs follow consistent patterns: - Configuration with API key, endpoint, timeout, retries - Custom exception types with error codes - Retry logic with exponential backoff - Health check endpoints - Closed state management
810 lines
21 KiB
Go
810 lines
21 KiB
Go
// 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<<attempt) * time.Second)
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
|
|
url := c.config.Endpoint + path
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
jsonBody, err := json.Marshal(body)
|
|
if err != nil {
|
|
return &Error{Message: fmt.Sprintf("failed to marshal body: %v", err)}
|
|
}
|
|
bodyReader = bytes.NewReader(jsonBody)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
|
if err != nil {
|
|
return &Error{Message: fmt.Sprintf("failed to create request: %v", err)}
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-SDK-Version", "go/0.1.0")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return &Error{Message: fmt.Sprintf("request failed: %v", err)}
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return &Error{Message: fmt.Sprintf("failed to read response: %v", err)}
|
|
}
|
|
|
|
if resp.StatusCode >= 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
|
|
}
|