|
|
|
@ -36,7 +36,6 @@ import ( |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
var ( |
|
|
|
|
errOnlyOnMainChain = errors.New("this operation is only available for blocks on the canonical chain") |
|
|
|
|
errBlockInvariant = errors.New("block objects must be instantiated with at least one of num or hash") |
|
|
|
|
) |
|
|
|
|
|
|
|
|
@ -44,12 +43,12 @@ var ( |
|
|
|
|
type Account struct { |
|
|
|
|
backend ethapi.Backend |
|
|
|
|
address common.Address |
|
|
|
|
blockNumber rpc.BlockNumber |
|
|
|
|
blockNrOrHash rpc.BlockNumberOrHash |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// getState fetches the StateDB object for an account.
|
|
|
|
|
func (a *Account) getState(ctx context.Context) (*state.StateDB, error) { |
|
|
|
|
state, _, err := a.backend.StateAndHeaderByNumber(ctx, a.blockNumber) |
|
|
|
|
state, _, err := a.backend.StateAndHeaderByNumberOrHash(ctx, a.blockNrOrHash) |
|
|
|
|
return state, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -104,7 +103,7 @@ func (l *Log) Account(ctx context.Context, args BlockNumberArgs) *Account { |
|
|
|
|
return &Account{ |
|
|
|
|
backend: l.backend, |
|
|
|
|
address: l.log.Address, |
|
|
|
|
blockNumber: args.Number(), |
|
|
|
|
blockNrOrHash: args.NumberOrLatest(), |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -136,10 +135,10 @@ func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) { |
|
|
|
|
tx, blockHash, _, index := rawdb.ReadTransaction(t.backend.ChainDb(), t.hash) |
|
|
|
|
if tx != nil { |
|
|
|
|
t.tx = tx |
|
|
|
|
blockNrOrHash := rpc.BlockNumberOrHashWithHash(blockHash, false) |
|
|
|
|
t.block = &Block{ |
|
|
|
|
backend: t.backend, |
|
|
|
|
hash: blockHash, |
|
|
|
|
canonical: unknown, |
|
|
|
|
numberOrHash: &blockNrOrHash, |
|
|
|
|
} |
|
|
|
|
t.index = index |
|
|
|
|
} else { |
|
|
|
@ -205,7 +204,7 @@ func (t *Transaction) To(ctx context.Context, args BlockNumberArgs) (*Account, e |
|
|
|
|
return &Account{ |
|
|
|
|
backend: t.backend, |
|
|
|
|
address: *to, |
|
|
|
|
blockNumber: args.Number(), |
|
|
|
|
blockNrOrHash: args.NumberOrLatest(), |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -223,7 +222,7 @@ func (t *Transaction) From(ctx context.Context, args BlockNumberArgs) (*Account, |
|
|
|
|
return &Account{ |
|
|
|
|
backend: t.backend, |
|
|
|
|
address: from, |
|
|
|
|
blockNumber: args.Number(), |
|
|
|
|
blockNrOrHash: args.NumberOrLatest(), |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -295,7 +294,7 @@ func (t *Transaction) CreatedContract(ctx context.Context, args BlockNumberArgs) |
|
|
|
|
return &Account{ |
|
|
|
|
backend: t.backend, |
|
|
|
|
address: receipt.ContractAddress, |
|
|
|
|
blockNumber: args.Number(), |
|
|
|
|
blockNrOrHash: args.NumberOrLatest(), |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -317,45 +316,16 @@ func (t *Transaction) Logs(ctx context.Context) (*[]*Log, error) { |
|
|
|
|
|
|
|
|
|
type BlockType int |
|
|
|
|
|
|
|
|
|
const ( |
|
|
|
|
unknown BlockType = iota |
|
|
|
|
isCanonical |
|
|
|
|
notCanonical |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// Block represents an Ethereum block.
|
|
|
|
|
// backend, and either num or hash are mandatory. All other fields are lazily fetched
|
|
|
|
|
// backend, and numberOrHash are mandatory. All other fields are lazily fetched
|
|
|
|
|
// when required.
|
|
|
|
|
type Block struct { |
|
|
|
|
backend ethapi.Backend |
|
|
|
|
num *rpc.BlockNumber |
|
|
|
|
numberOrHash *rpc.BlockNumberOrHash |
|
|
|
|
hash common.Hash |
|
|
|
|
header *types.Header |
|
|
|
|
block *types.Block |
|
|
|
|
receipts []*types.Receipt |
|
|
|
|
canonical BlockType // Indicates if this block is on the main chain or not.
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *Block) onMainChain(ctx context.Context) error { |
|
|
|
|
if b.canonical == unknown { |
|
|
|
|
header, err := b.resolveHeader(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
canonHeader, err := b.backend.HeaderByNumber(ctx, rpc.BlockNumber(header.Number.Uint64())) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
if header.Hash() == canonHeader.Hash() { |
|
|
|
|
b.canonical = isCanonical |
|
|
|
|
} else { |
|
|
|
|
b.canonical = notCanonical |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if b.canonical != isCanonical { |
|
|
|
|
return errOnlyOnMainChain |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// resolve returns the internal Block object representing this block, fetching
|
|
|
|
@ -364,14 +334,17 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) { |
|
|
|
|
if b.block != nil { |
|
|
|
|
return b.block, nil |
|
|
|
|
} |
|
|
|
|
var err error |
|
|
|
|
if b.hash != (common.Hash{}) { |
|
|
|
|
b.block, err = b.backend.BlockByHash(ctx, b.hash) |
|
|
|
|
} else { |
|
|
|
|
b.block, err = b.backend.BlockByNumber(ctx, *b.num) |
|
|
|
|
if b.numberOrHash == nil { |
|
|
|
|
latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) |
|
|
|
|
b.numberOrHash = &latest |
|
|
|
|
} |
|
|
|
|
var err error |
|
|
|
|
b.block, err = b.backend.BlockByNumberOrHash(ctx, *b.numberOrHash) |
|
|
|
|
if b.block != nil && b.header == nil { |
|
|
|
|
b.header = b.block.Header() |
|
|
|
|
if hash, ok := b.numberOrHash.Hash(); ok { |
|
|
|
|
b.hash = hash |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return b.block, err |
|
|
|
|
} |
|
|
|
@ -380,7 +353,7 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) { |
|
|
|
|
// if necessary. Call this function instead of `resolve` unless you need the
|
|
|
|
|
// additional data (transactions and uncles).
|
|
|
|
|
func (b *Block) resolveHeader(ctx context.Context) (*types.Header, error) { |
|
|
|
|
if b.num == nil && b.hash == (common.Hash{}) { |
|
|
|
|
if b.numberOrHash == nil && b.hash == (common.Hash{}) { |
|
|
|
|
return nil, errBlockInvariant |
|
|
|
|
} |
|
|
|
|
var err error |
|
|
|
@ -388,7 +361,7 @@ func (b *Block) resolveHeader(ctx context.Context) (*types.Header, error) { |
|
|
|
|
if b.hash != (common.Hash{}) { |
|
|
|
|
b.header, err = b.backend.HeaderByHash(ctx, b.hash) |
|
|
|
|
} else { |
|
|
|
|
b.header, err = b.backend.HeaderByNumber(ctx, *b.num) |
|
|
|
|
b.header, err = b.backend.HeaderByNumberOrHash(ctx, *b.numberOrHash) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return b.header, err |
|
|
|
@ -416,15 +389,12 @@ func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *Block) Number(ctx context.Context) (hexutil.Uint64, error) { |
|
|
|
|
if b.num == nil || *b.num == rpc.LatestBlockNumber { |
|
|
|
|
header, err := b.resolveHeader(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return 0, err |
|
|
|
|
} |
|
|
|
|
num := rpc.BlockNumber(header.Number.Uint64()) |
|
|
|
|
b.num = &num |
|
|
|
|
} |
|
|
|
|
return hexutil.Uint64(*b.num), nil |
|
|
|
|
|
|
|
|
|
return hexutil.Uint64(header.Number.Uint64()), nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *Block) Hash(ctx context.Context) (common.Hash, error) { |
|
|
|
@ -456,26 +426,17 @@ func (b *Block) GasUsed(ctx context.Context) (hexutil.Uint64, error) { |
|
|
|
|
|
|
|
|
|
func (b *Block) Parent(ctx context.Context) (*Block, error) { |
|
|
|
|
// If the block header hasn't been fetched, and we'll need it, fetch it.
|
|
|
|
|
if b.num == nil && b.hash != (common.Hash{}) && b.header == nil { |
|
|
|
|
if b.numberOrHash == nil && b.header == nil { |
|
|
|
|
if _, err := b.resolveHeader(ctx); err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if b.header != nil && b.header.Number.Uint64() > 0 { |
|
|
|
|
num := rpc.BlockNumber(b.header.Number.Uint64() - 1) |
|
|
|
|
num := rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(b.header.Number.Uint64() - 1)) |
|
|
|
|
return &Block{ |
|
|
|
|
backend: b.backend, |
|
|
|
|
num: &num, |
|
|
|
|
numberOrHash: &num, |
|
|
|
|
hash: b.header.ParentHash, |
|
|
|
|
canonical: unknown, |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
if b.num != nil && *b.num != 0 { |
|
|
|
|
num := *b.num - 1 |
|
|
|
|
return &Block{ |
|
|
|
|
backend: b.backend, |
|
|
|
|
num: &num, |
|
|
|
|
canonical: isCanonical, |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
return nil, nil |
|
|
|
@ -561,13 +522,11 @@ func (b *Block) Ommers(ctx context.Context) (*[]*Block, error) { |
|
|
|
|
} |
|
|
|
|
ret := make([]*Block, 0, len(block.Uncles())) |
|
|
|
|
for _, uncle := range block.Uncles() { |
|
|
|
|
blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) |
|
|
|
|
blockNumberOrHash := rpc.BlockNumberOrHashWithHash(uncle.Hash(), false) |
|
|
|
|
ret = append(ret, &Block{ |
|
|
|
|
backend: b.backend, |
|
|
|
|
num: &blockNumber, |
|
|
|
|
hash: uncle.Hash(), |
|
|
|
|
numberOrHash: &blockNumberOrHash, |
|
|
|
|
header: uncle, |
|
|
|
|
canonical: notCanonical, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
return &ret, nil |
|
|
|
@ -603,16 +562,26 @@ func (b *Block) TotalDifficulty(ctx context.Context) (hexutil.Big, error) { |
|
|
|
|
|
|
|
|
|
// BlockNumberArgs encapsulates arguments to accessors that specify a block number.
|
|
|
|
|
type BlockNumberArgs struct { |
|
|
|
|
// TODO: Ideally we could use input unions to allow the query to specify the
|
|
|
|
|
// block parameter by hash, block number, or tag but input unions aren't part of the
|
|
|
|
|
// standard GraphQL schema SDL yet, see: https://github.com/graphql/graphql-spec/issues/488
|
|
|
|
|
Block *hexutil.Uint64 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Number returns the provided block number, or rpc.LatestBlockNumber if none
|
|
|
|
|
// NumberOr returns the provided block number argument, or the "current" block number or hash if none
|
|
|
|
|
// was provided.
|
|
|
|
|
func (a BlockNumberArgs) Number() rpc.BlockNumber { |
|
|
|
|
func (a BlockNumberArgs) NumberOr(current rpc.BlockNumberOrHash) rpc.BlockNumberOrHash { |
|
|
|
|
if a.Block != nil { |
|
|
|
|
return rpc.BlockNumber(*a.Block) |
|
|
|
|
blockNr := rpc.BlockNumber(*a.Block) |
|
|
|
|
return rpc.BlockNumberOrHashWithNumber(blockNr) |
|
|
|
|
} |
|
|
|
|
return rpc.LatestBlockNumber |
|
|
|
|
return current |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// NumberOrLatest returns the provided block number argument, or the "latest" block number if none
|
|
|
|
|
// was provided.
|
|
|
|
|
func (a BlockNumberArgs) NumberOrLatest() rpc.BlockNumberOrHash { |
|
|
|
|
return a.NumberOr(rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *Block) Miner(ctx context.Context, args BlockNumberArgs) (*Account, error) { |
|
|
|
@ -623,7 +592,7 @@ func (b *Block) Miner(ctx context.Context, args BlockNumberArgs) (*Account, erro |
|
|
|
|
return &Account{ |
|
|
|
|
backend: b.backend, |
|
|
|
|
address: header.Coinbase, |
|
|
|
|
blockNumber: args.Number(), |
|
|
|
|
blockNrOrHash: args.NumberOrLatest(), |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -683,13 +652,11 @@ func (b *Block) OmmerAt(ctx context.Context, args struct{ Index int32 }) (*Block |
|
|
|
|
return nil, nil |
|
|
|
|
} |
|
|
|
|
uncle := uncles[args.Index] |
|
|
|
|
blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) |
|
|
|
|
blockNumberOrHash := rpc.BlockNumberOrHashWithHash(uncle.Hash(), false) |
|
|
|
|
return &Block{ |
|
|
|
|
backend: b.backend, |
|
|
|
|
num: &blockNumber, |
|
|
|
|
hash: uncle.Hash(), |
|
|
|
|
numberOrHash: &blockNumberOrHash, |
|
|
|
|
header: uncle, |
|
|
|
|
canonical: notCanonical, |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -757,11 +724,7 @@ func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteri |
|
|
|
|
func (b *Block) Account(ctx context.Context, args struct { |
|
|
|
|
Address common.Address |
|
|
|
|
}) (*Account, error) { |
|
|
|
|
err := b.onMainChain(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
if b.num == nil { |
|
|
|
|
if b.numberOrHash == nil { |
|
|
|
|
_, err := b.resolveHeader(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
@ -770,7 +733,7 @@ func (b *Block) Account(ctx context.Context, args struct { |
|
|
|
|
return &Account{ |
|
|
|
|
backend: b.backend, |
|
|
|
|
address: args.Address, |
|
|
|
|
blockNumber: *b.num, |
|
|
|
|
blockNrOrHash: *b.numberOrHash, |
|
|
|
|
}, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -807,17 +770,13 @@ func (c *CallResult) Status() hexutil.Uint64 { |
|
|
|
|
func (b *Block) Call(ctx context.Context, args struct { |
|
|
|
|
Data ethapi.CallArgs |
|
|
|
|
}) (*CallResult, error) { |
|
|
|
|
err := b.onMainChain(ctx) |
|
|
|
|
if b.numberOrHash == nil { |
|
|
|
|
_, err := b.resolve(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
if b.num == nil { |
|
|
|
|
_, err := b.resolveHeader(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
result, gas, failed, err := ethapi.DoCall(ctx, b.backend, args.Data, *b.num, nil, vm.Config{}, 5*time.Second, b.backend.RPCGasCap()) |
|
|
|
|
result, gas, failed, err := ethapi.DoCall(ctx, b.backend, args.Data, *b.numberOrHash, nil, vm.Config{}, 5*time.Second, b.backend.RPCGasCap()) |
|
|
|
|
status := hexutil.Uint64(1) |
|
|
|
|
if failed { |
|
|
|
|
status = 0 |
|
|
|
@ -832,17 +791,13 @@ func (b *Block) Call(ctx context.Context, args struct { |
|
|
|
|
func (b *Block) EstimateGas(ctx context.Context, args struct { |
|
|
|
|
Data ethapi.CallArgs |
|
|
|
|
}) (hexutil.Uint64, error) { |
|
|
|
|
err := b.onMainChain(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return hexutil.Uint64(0), err |
|
|
|
|
} |
|
|
|
|
if b.num == nil { |
|
|
|
|
if b.numberOrHash == nil { |
|
|
|
|
_, err := b.resolveHeader(ctx) |
|
|
|
|
if err != nil { |
|
|
|
|
return hexutil.Uint64(0), err |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
gas, err := ethapi.DoEstimateGas(ctx, b.backend, args.Data, *b.num, b.backend.RPCGasCap()) |
|
|
|
|
gas, err := ethapi.DoEstimateGas(ctx, b.backend, args.Data, *b.numberOrHash, b.backend.RPCGasCap()) |
|
|
|
|
return gas, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -875,17 +830,19 @@ func (p *Pending) Transactions(ctx context.Context) (*[]*Transaction, error) { |
|
|
|
|
func (p *Pending) Account(ctx context.Context, args struct { |
|
|
|
|
Address common.Address |
|
|
|
|
}) *Account { |
|
|
|
|
pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) |
|
|
|
|
return &Account{ |
|
|
|
|
backend: p.backend, |
|
|
|
|
address: args.Address, |
|
|
|
|
blockNumber: rpc.PendingBlockNumber, |
|
|
|
|
blockNrOrHash: pendingBlockNr, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (p *Pending) Call(ctx context.Context, args struct { |
|
|
|
|
Data ethapi.CallArgs |
|
|
|
|
}) (*CallResult, error) { |
|
|
|
|
result, gas, failed, err := ethapi.DoCall(ctx, p.backend, args.Data, rpc.PendingBlockNumber, nil, vm.Config{}, 5*time.Second, p.backend.RPCGasCap()) |
|
|
|
|
pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) |
|
|
|
|
result, gas, failed, err := ethapi.DoCall(ctx, p.backend, args.Data, pendingBlockNr, nil, vm.Config{}, 5*time.Second, p.backend.RPCGasCap()) |
|
|
|
|
status := hexutil.Uint64(1) |
|
|
|
|
if failed { |
|
|
|
|
status = 0 |
|
|
|
@ -900,7 +857,8 @@ func (p *Pending) Call(ctx context.Context, args struct { |
|
|
|
|
func (p *Pending) EstimateGas(ctx context.Context, args struct { |
|
|
|
|
Data ethapi.CallArgs |
|
|
|
|
}) (hexutil.Uint64, error) { |
|
|
|
|
return ethapi.DoEstimateGas(ctx, p.backend, args.Data, rpc.PendingBlockNumber, p.backend.RPCGasCap()) |
|
|
|
|
pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) |
|
|
|
|
return ethapi.DoEstimateGas(ctx, p.backend, args.Data, pendingBlockNr, p.backend.RPCGasCap()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Resolver is the top-level object in the GraphQL hierarchy.
|
|
|
|
@ -914,24 +872,23 @@ func (r *Resolver) Block(ctx context.Context, args struct { |
|
|
|
|
}) (*Block, error) { |
|
|
|
|
var block *Block |
|
|
|
|
if args.Number != nil { |
|
|
|
|
num := rpc.BlockNumber(uint64(*args.Number)) |
|
|
|
|
number := rpc.BlockNumber(uint64(*args.Number)) |
|
|
|
|
numberOrHash := rpc.BlockNumberOrHashWithNumber(number) |
|
|
|
|
block = &Block{ |
|
|
|
|
backend: r.backend, |
|
|
|
|
num: &num, |
|
|
|
|
canonical: isCanonical, |
|
|
|
|
numberOrHash: &numberOrHash, |
|
|
|
|
} |
|
|
|
|
} else if args.Hash != nil { |
|
|
|
|
numberOrHash := rpc.BlockNumberOrHashWithHash(*args.Hash, false) |
|
|
|
|
block = &Block{ |
|
|
|
|
backend: r.backend, |
|
|
|
|
hash: *args.Hash, |
|
|
|
|
canonical: unknown, |
|
|
|
|
numberOrHash: &numberOrHash, |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
num := rpc.LatestBlockNumber |
|
|
|
|
numberOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) |
|
|
|
|
block = &Block{ |
|
|
|
|
backend: r.backend, |
|
|
|
|
num: &num, |
|
|
|
|
canonical: isCanonical, |
|
|
|
|
numberOrHash: &numberOrHash, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// Resolve the header, return nil if it doesn't exist.
|
|
|
|
@ -963,11 +920,10 @@ func (r *Resolver) Blocks(ctx context.Context, args struct { |
|
|
|
|
} |
|
|
|
|
ret := make([]*Block, 0, to-from+1) |
|
|
|
|
for i := from; i <= to; i++ { |
|
|
|
|
num := i |
|
|
|
|
numberOrHash := rpc.BlockNumberOrHashWithNumber(i) |
|
|
|
|
ret = append(ret, &Block{ |
|
|
|
|
backend: r.backend, |
|
|
|
|
num: &num, |
|
|
|
|
canonical: isCanonical, |
|
|
|
|
numberOrHash: &numberOrHash, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
return ret, nil |
|
|
|
|