Commit 113e2744 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger, op-dispute-mon: Support both old and new versions of the...

op-challenger, op-dispute-mon: Support both old and new versions of the dispute game contracts (#10302)

* op-challenger, op-dispute-mon: Support both old and new versions of the dispute game contracts.

* op-challenger: Update fault dispute game contract tests to cover multiple versions.
parent ba174f4d
package main
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/flags"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
"github.com/ethereum-optimism/optimism/op-challenger/tools"
opservice "github.com/ethereum-optimism/optimism/op-service"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/urfave/cli/v2"
......@@ -38,7 +41,10 @@ func CreateGame(ctx *cli.Context) error {
traceType := ctx.Uint64(TraceTypeFlag.Name)
l2BlockNum := ctx.Uint64(L2BlockNumFlag.Name)
contract, txMgr, err := NewContractWithTxMgr[*contracts.DisputeGameFactoryContract](ctx, flags.FactoryAddressFlag.Name, contracts.NewDisputeGameFactoryContract)
contract, txMgr, err := NewContractWithTxMgr[*contracts.DisputeGameFactoryContract](ctx, flags.FactoryAddressFlag.Name,
func(ctx context.Context, metricer contractMetrics.ContractMetricer, address common.Address, caller *batching.MultiCaller) (*contracts.DisputeGameFactoryContract, error) {
return contracts.NewDisputeGameFactoryContract(metricer, address, caller), nil
})
if err != nil {
return fmt.Errorf("failed to create dispute game factory bindings: %w", err)
}
......
......@@ -48,11 +48,14 @@ func ListClaims(ctx *cli.Context) error {
defer l1Client.Close()
caller := batching.NewMultiCaller(l1Client.Client(), batching.DefaultBatchSize)
contract := contracts.NewFaultDisputeGameContract(metrics.NoopContractMetrics, gameAddr, caller)
contract, err := contracts.NewFaultDisputeGameContract(ctx.Context, metrics.NoopContractMetrics, gameAddr, caller)
if err != nil {
return err
}
return listClaims(ctx.Context, contract)
}
func listClaims(ctx context.Context, game *contracts.FaultDisputeGameContract) error {
func listClaims(ctx context.Context, game contracts.FaultDisputeGameContract) error {
maxDepth, err := game.GetMaxGameDepth(ctx)
if err != nil {
return fmt.Errorf("failed to retrieve max depth: %w", err)
......
......@@ -66,7 +66,10 @@ func listGames(ctx context.Context, caller *batching.MultiCaller, factory *contr
infos := make([]*gameInfo, len(games))
var wg sync.WaitGroup
for idx, game := range games {
gameContract := contracts.NewFaultDisputeGameContract(metrics.NoopContractMetrics, game.Proxy, caller)
gameContract, err := contracts.NewFaultDisputeGameContract(ctx, metrics.NoopContractMetrics, game.Proxy, caller)
if err != nil {
return fmt.Errorf("failed to create dispute game contract: %w", err)
}
info := gameInfo{GameMetadata: game}
infos[idx] = &info
gameProxy := game.Proxy
......
......@@ -46,7 +46,7 @@ func Move(ctx *cli.Context) error {
return fmt.Errorf("both attack and defense flags cannot be set")
}
contract, txMgr, err := NewContractWithTxMgr[*contracts.FaultDisputeGameContract](ctx, GameAddressFlag.Name, contracts.NewFaultDisputeGameContract)
contract, txMgr, err := NewContractWithTxMgr[contracts.FaultDisputeGameContract](ctx, GameAddressFlag.Name, contracts.NewFaultDisputeGameContract)
if err != nil {
return fmt.Errorf("failed to create dispute game bindings: %w", err)
}
......
......@@ -12,7 +12,7 @@ import (
)
func Resolve(ctx *cli.Context) error {
contract, txMgr, err := NewContractWithTxMgr[*contracts.FaultDisputeGameContract](ctx, GameAddressFlag.Name, contracts.NewFaultDisputeGameContract)
contract, txMgr, err := NewContractWithTxMgr[contracts.FaultDisputeGameContract](ctx, GameAddressFlag.Name, contracts.NewFaultDisputeGameContract)
if err != nil {
return fmt.Errorf("failed to create dispute game bindings: %w", err)
}
......
package main
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/flags"
......@@ -14,7 +15,7 @@ import (
"github.com/urfave/cli/v2"
)
type ContractCreator[T any] func(contractMetrics.ContractMetricer, common.Address, *batching.MultiCaller) T
type ContractCreator[T any] func(context.Context, contractMetrics.ContractMetricer, common.Address, *batching.MultiCaller) (T, error)
// NewContractWithTxMgr creates a new contract and a transaction manager.
func NewContractWithTxMgr[T any](ctx *cli.Context, flagName string, creator ContractCreator[T]) (T, txmgr.TxManager, error) {
......@@ -40,7 +41,10 @@ func newContractFromCLI[T any](ctx *cli.Context, flagName string, caller *batchi
return contract, err
}
created := creator(contractMetrics.NoopContractMetrics, gameAddr, caller)
created, err := creator(ctx.Context, contractMetrics.NoopContractMetrics, gameAddr, caller)
if err != nil {
return contract, fmt.Errorf("failed to create contract bindings: %w", err)
}
return created, nil
}
......
package contracts
import (
"context"
_ "embed"
"fmt"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
)
//go:embed abis/FaultDisputeGame-0.8.0.json
var faultDisputeGameAbi020 []byte
var resolvedBondAmount = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big.NewInt(1))
var (
methodGameDuration = "gameDuration"
)
type FaultDisputeGameContract080 struct {
FaultDisputeGameContractLatest
}
// GetGameMetadata returns the game's L1 head, L2 block number, root claim, status, and max clock duration.
func (f *FaultDisputeGameContract080) GetGameMetadata(ctx context.Context, block rpcblock.Block) (common.Hash, uint64, common.Hash, gameTypes.GameStatus, uint64, error) {
defer f.metrics.StartContractRequest("GetGameMetadata")()
results, err := f.multiCaller.Call(ctx, block,
f.contract.Call(methodL1Head),
f.contract.Call(methodL2BlockNumber),
f.contract.Call(methodRootClaim),
f.contract.Call(methodStatus),
f.contract.Call(methodGameDuration))
if err != nil {
return common.Hash{}, 0, common.Hash{}, 0, 0, fmt.Errorf("failed to retrieve game metadata: %w", err)
}
if len(results) != 5 {
return common.Hash{}, 0, common.Hash{}, 0, 0, fmt.Errorf("expected 3 results but got %v", len(results))
}
l1Head := results[0].GetHash(0)
l2BlockNumber := results[1].GetBigInt(0).Uint64()
rootClaim := results[2].GetHash(0)
status, err := gameTypes.GameStatusFromUint8(results[3].GetUint8(0))
if err != nil {
return common.Hash{}, 0, common.Hash{}, 0, 0, fmt.Errorf("failed to convert game status: %w", err)
}
duration := results[4].GetUint64(0)
return l1Head, l2BlockNumber, rootClaim, status, duration / 2, nil
}
func (f *FaultDisputeGameContract080) GetMaxClockDuration(ctx context.Context) (time.Duration, error) {
defer f.metrics.StartContractRequest("GetMaxClockDuration")()
result, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodGameDuration))
if err != nil {
return 0, fmt.Errorf("failed to fetch game duration: %w", err)
}
return time.Duration(result.GetUint64(0)) * time.Second / 2, nil
}
func (f *FaultDisputeGameContract080) GetClaim(ctx context.Context, idx uint64) (types.Claim, error) {
claim, err := f.FaultDisputeGameContractLatest.GetClaim(ctx, idx)
if err != nil {
return types.Claim{}, err
}
// Replace the resolved sentinel with what the bond would have been
if claim.Bond.Cmp(resolvedBondAmount) == 0 {
bond, err := f.GetRequiredBond(ctx, claim.Position)
if err != nil {
return types.Claim{}, err
}
claim.Bond = bond
}
return claim, nil
}
func (f *FaultDisputeGameContract080) GetAllClaims(ctx context.Context, block rpcblock.Block) ([]types.Claim, error) {
claims, err := f.FaultDisputeGameContractLatest.GetAllClaims(ctx, block)
if err != nil {
return nil, err
}
resolvedClaims := make([]*types.Claim, 0, len(claims))
positions := make([]*big.Int, 0, len(claims))
for i, claim := range claims {
if claim.Bond.Cmp(resolvedBondAmount) == 0 {
resolvedClaims = append(resolvedClaims, &claims[i])
positions = append(positions, claim.Position.ToGIndex())
}
}
bonds, err := f.GetRequiredBonds(ctx, block, positions...)
if err != nil {
return nil, fmt.Errorf("failed to get required bonds for resolved claims: %w", err)
}
for i, bond := range bonds {
resolvedClaims[i].Bond = bond
}
return claims, nil
}
func (f *FaultDisputeGameContract080) IsResolved(ctx context.Context, block rpcblock.Block, claims ...types.Claim) ([]bool, error) {
rawClaims, err := f.FaultDisputeGameContractLatest.GetAllClaims(ctx, block)
if err != nil {
return nil, fmt.Errorf("failed to get raw claim data: %w", err)
}
results := make([]bool, len(claims))
for i, claim := range claims {
results[i] = rawClaims[claim.ContractIndex].Bond.Cmp(resolvedBondAmount) == 0
}
return results, nil
}
func (f *FaultDisputeGameContract080) CallResolveClaim(ctx context.Context, claimIdx uint64) error {
defer f.metrics.StartContractRequest("CallResolveClaim")()
call := f.resolveClaimCall(claimIdx)
_, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, call)
if err != nil {
return fmt.Errorf("failed to call resolve claim: %w", err)
}
return nil
}
func (f *FaultDisputeGameContract080) ResolveClaimTx(claimIdx uint64) (txmgr.TxCandidate, error) {
call := f.resolveClaimCall(claimIdx)
return call.ToTxCandidate()
}
func (f *FaultDisputeGameContract080) resolveClaimCall(claimIdx uint64) *batching.ContractCall {
return f.contract.Call(methodResolveClaim, new(big.Int).SetUint64(claimIdx))
}
......@@ -120,7 +120,10 @@ func registerAlphabet(
claimants []common.Address,
) error {
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract := contracts.NewFaultDisputeGameContract(m, game.Proxy, caller)
contract, err := contracts.NewFaultDisputeGameContract(ctx, m, game.Proxy, caller)
if err != nil {
return nil, fmt.Errorf("failed to create fault dispute game contract: %w", err)
}
oracle, err := contract.GetOracle(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load oracle for game %v: %w", game.Proxy, err)
......@@ -157,7 +160,7 @@ func registerAlphabet(
registry.RegisterGameType(faultTypes.AlphabetGameType, playerCreator)
contractCreator := func(game types.GameMetadata) (claims.BondContract, error) {
return contracts.NewFaultDisputeGameContract(m, game.Proxy, caller), nil
return contracts.NewFaultDisputeGameContract(ctx, m, game.Proxy, caller)
}
registry.RegisterBondContract(faultTypes.AlphabetGameType, contractCreator)
return nil
......@@ -168,7 +171,10 @@ func registerOracle(ctx context.Context, m metrics.Metricer, oracles OracleRegis
if err != nil {
return fmt.Errorf("failed to load implementation for game type %v: %w", gameType, err)
}
contract := contracts.NewFaultDisputeGameContract(m, implAddr, caller)
contract, err := contracts.NewFaultDisputeGameContract(ctx, m, implAddr, caller)
if err != nil {
return fmt.Errorf("failed to create fault dispute game contracts: %w", err)
}
oracle, err := contract.GetOracle(ctx)
if err != nil {
return fmt.Errorf("failed to load oracle address: %w", err)
......@@ -199,7 +205,10 @@ func registerAsterisc(
) error {
asteriscPrestateProvider := asterisc.NewPrestateProvider(cfg.AsteriscAbsolutePreState)
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract := contracts.NewFaultDisputeGameContract(m, game.Proxy, caller)
contract, err := contracts.NewFaultDisputeGameContract(ctx, m, game.Proxy, caller)
if err != nil {
return nil, fmt.Errorf("failed to create fault dispute game contracts: %w", err)
}
oracle, err := contract.GetOracle(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load oracle for game %v: %w", game.Proxy, err)
......@@ -236,7 +245,7 @@ func registerAsterisc(
registry.RegisterGameType(gameType, playerCreator)
contractCreator := func(game types.GameMetadata) (claims.BondContract, error) {
return contracts.NewFaultDisputeGameContract(m, game.Proxy, caller), nil
return contracts.NewFaultDisputeGameContract(ctx, m, game.Proxy, caller)
}
registry.RegisterBondContract(gameType, contractCreator)
return nil
......@@ -276,7 +285,10 @@ func registerCannon(
return cannon.NewPrestateProvider(prestatePath), nil
})
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract := contracts.NewFaultDisputeGameContract(m, game.Proxy, caller)
contract, err := contracts.NewFaultDisputeGameContract(ctx, m, game.Proxy, caller)
if err != nil {
return nil, fmt.Errorf("failed to create fault dispute game contracts: %w", err)
}
requiredPrestatehash, err := contract.GetAbsolutePrestateHash(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load prestate hash for game %v: %w", game.Proxy, err)
......@@ -324,13 +336,13 @@ func registerCannon(
registry.RegisterGameType(gameType, playerCreator)
contractCreator := func(game types.GameMetadata) (claims.BondContract, error) {
return contracts.NewFaultDisputeGameContract(m, game.Proxy, caller), nil
return contracts.NewFaultDisputeGameContract(ctx, m, game.Proxy, caller)
}
registry.RegisterBondContract(gameType, contractCreator)
return nil
}
func loadL1Head(contract *contracts.FaultDisputeGameContract, ctx context.Context, l1HeaderSource L1HeaderSource) (eth.BlockID, error) {
func loadL1Head(contract contracts.FaultDisputeGameContract, ctx context.Context, l1HeaderSource L1HeaderSource) (eth.BlockID, error) {
l1Head, err := contract.GetL1Head(ctx)
if err != nil {
return eth.BlockID{}, fmt.Errorf("failed to load L1 head: %w", err)
......
......@@ -34,7 +34,7 @@ type GameCaller interface {
type GameCallerCreator struct {
m GameCallerMetrics
cache *caching.LRUCache[common.Address, *contracts.FaultDisputeGameContract]
cache *caching.LRUCache[common.Address, contracts.FaultDisputeGameContract]
caller *batching.MultiCaller
}
......@@ -42,17 +42,20 @@ func NewGameCallerCreator(m GameCallerMetrics, caller *batching.MultiCaller) *Ga
return &GameCallerCreator{
m: m,
caller: caller,
cache: caching.NewLRUCache[common.Address, *contracts.FaultDisputeGameContract](m, metricsLabel, 100),
cache: caching.NewLRUCache[common.Address, contracts.FaultDisputeGameContract](m, metricsLabel, 100),
}
}
func (g *GameCallerCreator) CreateContract(game gameTypes.GameMetadata) (GameCaller, error) {
func (g *GameCallerCreator) CreateContract(ctx context.Context, game gameTypes.GameMetadata) (GameCaller, error) {
if fdg, ok := g.cache.Get(game.Proxy); ok {
return fdg, nil
}
switch game.GameType {
case faultTypes.CannonGameType, faultTypes.AsteriscGameType, faultTypes.AlphabetGameType:
fdg := contracts.NewFaultDisputeGameContract(g.m, game.Proxy, g.caller)
fdg, err := contracts.NewFaultDisputeGameContract(ctx, g.m, game.Proxy, g.caller)
if err != nil {
return nil, fmt.Errorf("failed to create fault dispute game contract: %w", err)
}
g.cache.Add(game.Proxy, fdg)
return fdg, nil
default:
......
package extract
import (
"context"
"fmt"
"testing"
contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
......@@ -49,13 +51,13 @@ func TestMetadataCreator_CreateContract(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
caller, metrics := setupMetadataLoaderTest(t)
creator := NewGameCallerCreator(metrics, caller)
_, err := creator.CreateContract(test.game)
_, err := creator.CreateContract(context.Background(), test.game)
require.Equal(t, test.expectedErr, err)
if test.expectedErr == nil {
require.Equal(t, 1, metrics.cacheAddCalls)
require.Equal(t, 1, metrics.cacheGetCalls)
}
_, err = creator.CreateContract(test.game)
_, err = creator.CreateContract(context.Background(), test.game)
require.Equal(t, test.expectedErr, err)
if test.expectedErr == nil {
require.Equal(t, 1, metrics.cacheAddCalls)
......@@ -70,6 +72,7 @@ func setupMetadataLoaderTest(t *testing.T) (*batching.MultiCaller, *mockCacheMet
require.NoError(t, err)
stubRpc := batchingTest.NewAbiBasedRpc(t, fdgAddr, fdgAbi)
caller := batching.NewMultiCaller(stubRpc, batching.DefaultBatchSize)
stubRpc.SetResponse(fdgAddr, "version", rpcblock.Latest, nil, []interface{}{"0.18.0"})
return caller, &mockCacheMetrics{}
}
......
......@@ -13,7 +13,7 @@ import (
)
type (
CreateGameCaller func(game gameTypes.GameMetadata) (GameCaller, error)
CreateGameCaller func(ctx context.Context, game gameTypes.GameMetadata) (GameCaller, error)
FactoryGameFetcher func(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]gameTypes.GameMetadata, error)
)
......@@ -48,7 +48,7 @@ func (e *Extractor) Extract(ctx context.Context, blockHash common.Hash, minTimes
func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, games []gameTypes.GameMetadata) []*monTypes.EnrichedGameData {
var enrichedGames []*monTypes.EnrichedGameData
for _, game := range games {
caller, err := e.createContract(game)
caller, err := e.createContract(ctx, game)
if err != nil {
e.logger.Error("Failed to create game caller", "err", err)
continue
......
......@@ -167,7 +167,7 @@ type mockGameCallerCreator struct {
caller *mockGameCaller
}
func (m *mockGameCallerCreator) CreateGameCaller(_ gameTypes.GameMetadata) (GameCaller, error) {
func (m *mockGameCallerCreator) CreateGameCaller(_ context.Context, _ gameTypes.GameMetadata) (GameCaller, error) {
m.calls++
if m.err != nil {
return nil, m.err
......
......@@ -39,7 +39,8 @@ func (g *OutputAlphabetGameHelper) StartChallenger(
func (g *OutputAlphabetGameHelper) CreateHonestActor(ctx context.Context, l2Node string) *OutputHonestHelper {
logger := testlog.Logger(g.T, log.LevelInfo).New("role", "HonestHelper", "game", g.Addr)
caller := batching.NewMultiCaller(g.System.NodeClient("l1").Client(), batching.DefaultBatchSize)
contract := contracts.NewFaultDisputeGameContract(contractMetrics.NoopContractMetrics, g.Addr, caller)
contract, err := contracts.NewFaultDisputeGameContract(ctx, contractMetrics.NoopContractMetrics, g.Addr, caller)
g.Require.NoError(err)
prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx)
g.Require.NoError(err, "Get block range")
splitDepth := g.SplitDepth(ctx)
......
......@@ -63,7 +63,8 @@ func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node s
logger := testlog.Logger(g.T, log.LevelInfo).New("role", "HonestHelper", "game", g.Addr)
l2Client := g.System.NodeClient(l2Node)
caller := batching.NewMultiCaller(g.System.NodeClient("l1").Client(), batching.DefaultBatchSize)
contract := contracts.NewFaultDisputeGameContract(contractMetrics.NoopContractMetrics, g.Addr, caller)
contract, err := contracts.NewFaultDisputeGameContract(ctx, contractMetrics.NoopContractMetrics, g.Addr, caller)
g.Require.NoError(err)
prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx)
g.Require.NoError(err, "Failed to load block range")
......@@ -288,7 +289,8 @@ func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context,
caller := batching.NewMultiCaller(g.System.NodeClient("l1").Client(), batching.DefaultBatchSize)
l2Client := g.System.NodeClient(l2Node)
contract := contracts.NewFaultDisputeGameContract(contractMetrics.NoopContractMetrics, g.Addr, caller)
contract, err := contracts.NewFaultDisputeGameContract(ctx, contractMetrics.NoopContractMetrics, g.Addr, caller)
g.Require.NoError(err)
prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx)
g.Require.NoError(err, "Failed to load block range")
......
......@@ -713,7 +713,8 @@ func (g *OutputGameHelper) UploadPreimage(ctx context.Context, data *types.Preim
func (g *OutputGameHelper) oracle(ctx context.Context) *contracts.PreimageOracleContract {
caller := batching.NewMultiCaller(g.System.NodeClient("l1").Client(), batching.DefaultBatchSize)
contract := contracts.NewFaultDisputeGameContract(contractMetrics.NoopContractMetrics, g.Addr, caller)
contract, err := contracts.NewFaultDisputeGameContract(ctx, contractMetrics.NoopContractMetrics, g.Addr, caller)
g.Require.NoError(err)
oracle, err := contract.GetOracle(ctx)
g.Require.NoError(err, "Failed to create oracle contract")
return oracle
......
......@@ -17,11 +17,11 @@ type OutputHonestHelper struct {
t *testing.T
require *require.Assertions
game *OutputGameHelper
contract *contracts.FaultDisputeGameContract
contract contracts.FaultDisputeGameContract
correctTrace types.TraceAccessor
}
func NewOutputHonestHelper(t *testing.T, require *require.Assertions, game *OutputGameHelper, contract *contracts.FaultDisputeGameContract, correctTrace types.TraceAccessor) *OutputHonestHelper {
func NewOutputHonestHelper(t *testing.T, require *require.Assertions, game *OutputGameHelper, contract contracts.FaultDisputeGameContract, correctTrace types.TraceAccessor) *OutputHonestHelper {
return &OutputHonestHelper{
t: t,
require: require,
......
......@@ -63,3 +63,7 @@ func (c *CallResult) GetBytes32(i int) [32]byte {
func (c *CallResult) GetBytes32Slice(i int) [][32]byte {
return *abi.ConvertType(c.out[i], new([][32]byte)).(*[][32]byte)
}
func (c *CallResult) GetString(i int) string {
return *abi.ConvertType(c.out[i], new(string)).(*string)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment