From 969b09e2e450e24d2f63b42e74656d7a0aeaefa4 Mon Sep 17 00:00:00 2001 From: sequel21 Date: Sun, 12 Jan 2025 10:38:07 +0000 Subject: [PATCH] Expose functions for more control over emulator block generation --- asserts.go | 20 ++- connector.go | 45 +++++- emulator.go | 351 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- template_engine.go | 8 +- transaction.go | 6 +- 6 files changed, 419 insertions(+), 13 deletions(-) create mode 100644 emulator.go diff --git a/asserts.go b/asserts.go index 5ca98b5..ef8f56d 100644 --- a/asserts.go +++ b/asserts.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/exp/maps" ) @@ -19,11 +20,15 @@ type TransactionResult struct { func (tb FlowTransactionBuilder) Test(t *testing.T) TransactionResult { locale, _ := time.LoadLocation("UTC") time.Local = locale - events, err := tb.RunE(context.Background()) - formattedEvents := make([]*FormatedEvent, len(events)) - for i, event := range events { - ev := ParseEvent(event, uint64(0), time.Unix(0, 0), []string{}) - formattedEvents[i] = ev + txResult, err := tb.RunE(context.Background()) + var formattedEvents []*FormatedEvent + if err == nil { + events := txResult.Events + formattedEvents = make([]*FormatedEvent, len(events)) + for i, event := range events { + ev := ParseEvent(event, uint64(0), time.Unix(0, 0), []string{}) + formattedEvents[i] = ev + } } return TransactionResult{ Err: err, @@ -45,6 +50,11 @@ func (t TransactionResult) AssertSuccess() TransactionResult { return t } +func (t TransactionResult) RequireSuccess() TransactionResult { + require.NoError(t.Testing, t.Err) + return t +} + func (t TransactionResult) AssertEventCount(number int) TransactionResult { assert.Equal(t.Testing, number, len(t.Events)) return t diff --git a/connector.go b/connector.go index 8997b63..01f411a 100644 --- a/connector.go +++ b/connector.go @@ -1,17 +1,22 @@ package splash import ( + "errors" "fmt" "log" + "testing" "github.com/onflow/flow-emulator/emulator" "github.com/onflow/flow-go-sdk/access" grpcAccess "github.com/onflow/flow-go-sdk/access/grpc" + flowgo "github.com/onflow/flow-go/model/flow" "github.com/onflow/flowkit/v2" "github.com/onflow/flowkit/v2/accounts" "github.com/onflow/flowkit/v2/config" "github.com/onflow/flowkit/v2/gateway" "github.com/onflow/flowkit/v2/output" + "github.com/rs/zerolog" + zeroLog "github.com/rs/zerolog/log" "github.com/spf13/afero" "google.golang.org/grpc" ) @@ -24,6 +29,8 @@ type Connector struct { Network string Logger output.Logger PrependNetworkToAccountNames bool + + emulatorGateway *EmulatorGateway } // maxGRPCMessageSize 60mb @@ -82,11 +89,16 @@ func NewInMemoryConnector(paths []string, baseLoader flowkit.ReaderWriter, enabl SigAlgo: acc.Key.SigAlgo(), HashAlgo: acc.Key.HashAlgo(), } - var gw *gateway.EmulatorGateway + + loggerOpt := emulator.WithServerLogger( + zeroLog.Logger.Level(zerolog.InfoLevel).With().Str("module", "emulator").Logger(), + ) + + var gw *EmulatorGateway if enableTxFees { - gw = gateway.NewEmulatorGatewayWithOpts(key, gateway.WithEmulatorOptions(emulator.WithTransactionFeesEnabled(true))) + gw = NewEmulatorGatewayWithOpts(key, WithEmulatorOptions(loggerOpt, emulator.WithTransactionFeesEnabled(true))) } else { - gw = gateway.NewEmulatorGateway(key) + gw = NewEmulatorGatewayWithOpts(key, WithEmulatorOptions(loggerOpt)) } service := flowkit.NewFlowkit(state, config.EmulatorNetwork, gw, logger) @@ -96,6 +108,7 @@ func NewInMemoryConnector(paths []string, baseLoader flowkit.ReaderWriter, enabl Logger: logger, PrependNetworkToAccountNames: true, Network: "emulator", + emulatorGateway: gw, }, nil } @@ -128,3 +141,29 @@ func (c *Connector) Account(key string) *accounts.Account { return account } + +func (c *Connector) EnableAutoMine() *Connector { + if c.emulatorGateway != nil { + c.emulatorGateway.EnableAutoMine() + } + return c +} + +func (c *Connector) DisableAutoMine() *Connector { + if c.emulatorGateway != nil { + c.emulatorGateway.DisableAutoMine() + } + return c +} + +func (c *Connector) ExecuteAndCommitBlock(t *testing.T) (*flowgo.Block, []TransactionResult, error) { //nolint:thelper + if c.emulatorGateway != nil { + return c.emulatorGateway.ExecuteAndCommitBlock(t) + } else { + return nil, nil, errors.New("emulator gateway not initialized") + } +} + +func (c *Connector) IsInMemoryEmulator() bool { + return c.emulatorGateway != nil +} diff --git a/emulator.go b/emulator.go new file mode 100644 index 0000000..c7a711e --- /dev/null +++ b/emulator.go @@ -0,0 +1,351 @@ +package splash + +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/onflow/cadence" + jsoncdc "github.com/onflow/cadence/encoding/json" + "github.com/onflow/cadence/runtime" + "github.com/onflow/flow-emulator/adapters" + "github.com/onflow/flow-emulator/emulator" + "github.com/onflow/flow-go-sdk" + flowgo "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flowkit/v2/gateway" + "github.com/rs/zerolog" + "google.golang.org/grpc/status" +) + +type EmulatorGateway struct { + emulator *emulator.Blockchain + adapter *adapters.SDKAdapter + accessAdapter *adapters.AccessAdapter + logger *zerolog.Logger + emulatorOptions []emulator.Option +} + +func UnwrapStatusError(err error) error { + return errors.New(status.Convert(err).Message()) +} + +func NewEmulatorGateway(key *gateway.EmulatorKey) *EmulatorGateway { + return NewEmulatorGatewayWithOpts(key) +} + +func NewEmulatorGatewayWithOpts(key *gateway.EmulatorKey, opts ...func(*EmulatorGateway)) *EmulatorGateway { + noopLogger := zerolog.Nop() + gateway := &EmulatorGateway{ + logger: &noopLogger, + emulatorOptions: []emulator.Option{}, + } + for _, opt := range opts { + opt(gateway) + } + + gateway.emulator = newEmulator(key, gateway.emulatorOptions...) + gateway.adapter = adapters.NewSDKAdapter(gateway.logger, gateway.emulator) + gateway.accessAdapter = adapters.NewAccessAdapter(gateway.logger, gateway.emulator) + gateway.emulator.EnableAutoMine() + return gateway +} + +func WithLogger(logger *zerolog.Logger) func(g *EmulatorGateway) { + return func(g *EmulatorGateway) { + g.logger = logger + } +} + +func WithEmulatorOptions(options ...emulator.Option) func(g *EmulatorGateway) { + return func(g *EmulatorGateway) { + g.emulatorOptions = append(g.emulatorOptions, options...) + } +} + +func newEmulator(key *gateway.EmulatorKey, emulatorOptions ...emulator.Option) *emulator.Blockchain { + var opts []emulator.Option + + if key != nil { + opts = append(opts, emulator.WithServicePublicKey(key.PublicKey, key.SigAlgo, key.HashAlgo)) + } + + opts = append(opts, emulatorOptions...) + + b, err := emulator.New(opts...) + if err != nil { + panic(err) + } + + return b +} + +func (g *EmulatorGateway) GetAccount(ctx context.Context, address flow.Address) (*flow.Account, error) { + account, err := g.adapter.GetAccount(ctx, address) + if err != nil { + return nil, UnwrapStatusError(err) + } + return account, nil +} + +func (g *EmulatorGateway) SendSignedTransaction(ctx context.Context, tx *flow.Transaction) (*flow.Transaction, error) { + err := g.adapter.SendTransaction(ctx, *tx) + if err != nil { + return nil, UnwrapStatusError(err) + } + return tx, nil +} + +func (g *EmulatorGateway) GetTransactionResult(ctx context.Context, ID flow.Identifier, _ bool) (*flow.TransactionResult, error) { + result, err := g.adapter.GetTransactionResult(ctx, ID) + if err != nil { + return nil, UnwrapStatusError(err) + } + return result, nil +} + +func (g *EmulatorGateway) GetTransaction(ctx context.Context, id flow.Identifier) (*flow.Transaction, error) { + transaction, err := g.adapter.GetTransaction(ctx, id) + if err != nil { + return nil, UnwrapStatusError(err) + } + return transaction, nil +} + +func (g *EmulatorGateway) GetTransactionResultsByBlockID(ctx context.Context, id flow.Identifier) ([]*flow.TransactionResult, error) { + txr, err := g.adapter.GetTransactionResultsByBlockID(ctx, id) + if err != nil { + return nil, UnwrapStatusError(err) + } + return txr, nil +} + +func (g *EmulatorGateway) GetTransactionsByBlockID(ctx context.Context, id flow.Identifier) ([]*flow.Transaction, error) { + txr, err := g.adapter.GetTransactionsByBlockID(ctx, id) + if err != nil { + return nil, UnwrapStatusError(err) + } + return txr, nil +} + +func (g *EmulatorGateway) Ping() error { + ctx := context.Background() + err := g.adapter.Ping(ctx) + if err != nil { + return UnwrapStatusError(err) + } + return nil +} + +func (g *EmulatorGateway) WaitServer(ctx context.Context) error { + return nil +} + +type scriptQuery struct { + id flow.Identifier + height uint64 + latest bool +} + +func (g *EmulatorGateway) executeScriptQuery( + ctx context.Context, + script []byte, + arguments []cadence.Value, + query scriptQuery, +) (cadence.Value, error) { + args, err := cadenceValuesToMessages(arguments) + if err != nil { + return nil, UnwrapStatusError(err) + } + + var result []byte + if query.id != flow.EmptyID { + result, err = g.adapter.ExecuteScriptAtBlockID(ctx, query.id, script, args) + } else if query.height > 0 { + result, err = g.adapter.ExecuteScriptAtBlockHeight(ctx, query.height, script, args) + } else { + result, err = g.adapter.ExecuteScriptAtLatestBlock(ctx, script, args) + } + + if err != nil { + return nil, UnwrapStatusError(err) + } + + value, err := messageToCadenceValue(result) + if err != nil { + return nil, UnwrapStatusError(err) + } + + return value, nil +} + +func (g *EmulatorGateway) ExecuteScript( + ctx context.Context, + script []byte, + arguments []cadence.Value, +) (cadence.Value, error) { + return g.executeScriptQuery(ctx, script, arguments, scriptQuery{latest: true}) +} + +func (g *EmulatorGateway) ExecuteScriptAtHeight( + ctx context.Context, + script []byte, + arguments []cadence.Value, + height uint64, +) (cadence.Value, error) { + return g.executeScriptQuery(ctx, script, arguments, scriptQuery{height: height}) +} + +func (g *EmulatorGateway) ExecuteScriptAtID( + ctx context.Context, + script []byte, + arguments []cadence.Value, + id flow.Identifier, +) (cadence.Value, error) { + return g.executeScriptQuery(ctx, script, arguments, scriptQuery{id: id}) +} + +func (g *EmulatorGateway) GetLatestBlock(ctx context.Context) (*flow.Block, error) { + block, _, err := g.adapter.GetLatestBlock(ctx, true) + if err != nil { + return nil, UnwrapStatusError(err) + } + + return block, nil +} + +func cadenceValuesToMessages(values []cadence.Value) ([][]byte, error) { + msgs := make([][]byte, len(values)) + for i, val := range values { + msg, err := jsoncdc.Encode(val) + if err != nil { + return nil, fmt.Errorf("convert: %w", err) + } + msgs[i] = msg + } + return msgs, nil +} + +func messageToCadenceValue(m []byte) (cadence.Value, error) { + v, err := jsoncdc.Decode(nil, m) + if err != nil { + return nil, fmt.Errorf("convert: %w", err) + } + + return v, nil +} + +func (g *EmulatorGateway) GetEvents( + ctx context.Context, + eventType string, + startHeight uint64, + endHeight uint64, +) ([]flow.BlockEvents, error) { + events := make([]flow.BlockEvents, 0) + + for height := startHeight; height <= endHeight; height++ { + events = append(events, g.getBlockEvent(ctx, height, eventType)) + } + + return events, nil +} + +func (g *EmulatorGateway) getBlockEvent(ctx context.Context, height uint64, eventType string) flow.BlockEvents { + events, _ := g.adapter.GetEventsForHeightRange(ctx, eventType, height, height) + return *events[0] +} + +func (g *EmulatorGateway) GetCollection(ctx context.Context, id flow.Identifier) (*flow.Collection, error) { + collection, err := g.adapter.GetCollectionByID(ctx, id) + if err != nil { + return nil, UnwrapStatusError(err) + } + return collection, nil +} + +func (g *EmulatorGateway) GetBlockByID(ctx context.Context, id flow.Identifier) (*flow.Block, error) { + block, _, err := g.adapter.GetBlockByID(ctx, id) + if err != nil { + return nil, UnwrapStatusError(err) + } + return block, nil +} + +func (g *EmulatorGateway) GetBlockByHeight(ctx context.Context, height uint64) (*flow.Block, error) { + block, _, err := g.adapter.GetBlockByHeight(ctx, height) + if err != nil { + return nil, UnwrapStatusError(err) + } + return block, nil +} + +func (g *EmulatorGateway) GetLatestProtocolStateSnapshot(ctx context.Context) ([]byte, error) { + snapshot, err := g.adapter.GetLatestProtocolStateSnapshot(ctx) + if err != nil { + return nil, UnwrapStatusError(err) + } + return snapshot, nil +} + +// SecureConnection placeholder func to complete gateway interface implementation +func (g *EmulatorGateway) SecureConnection() bool { + return false +} + +func (g *EmulatorGateway) CoverageReport() *runtime.CoverageReport { + return g.emulator.CoverageReport() +} + +func (g *EmulatorGateway) RollbackToBlockHeight(height uint64) error { + return g.emulator.RollbackToBlockHeight(height) +} + +func (g *EmulatorGateway) EnableAutoMine() { + g.emulator.EnableAutoMine() +} + +func (g *EmulatorGateway) DisableAutoMine() { + g.emulator.DisableAutoMine() +} + +func (g *EmulatorGateway) ExecuteAndCommitBlock(t *testing.T) (*flowgo.Block, []TransactionResult, error) { //nolint:thelper + block, results, err := g.emulator.ExecuteAndCommitBlock() + if err != nil { + return nil, nil, err + } + + wrappedResults := make([]TransactionResult, len(results)) + for i, res := range results { + formattedEvents := make([]*FormatedEvent, len(res.Events)) + for i, event := range res.Events { + ev := ParseEvent(event, uint64(0), time.Unix(0, 0), []string{}) + formattedEvents[i] = ev + } + wrappedResults[i] = TransactionResult{ + Err: err, + Events: formattedEvents, + Testing: t, + } + } + + return block, wrappedResults, nil +} diff --git a/go.mod b/go.mod index 495ef0d..817bf48 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/onflow/cadence v1.2.1 github.com/onflow/flow-emulator v1.1.0 + github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23 github.com/onflow/flow-go-sdk v1.2.2 github.com/onflow/flowkit/v2 v2.1.0 github.com/rs/zerolog v1.33.0 @@ -154,7 +155,6 @@ require ( github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0 // indirect github.com/onflow/flow-ft/lib/go/contracts v1.0.1 // indirect github.com/onflow/flow-ft/lib/go/templates v1.0.1 // indirect - github.com/onflow/flow-go v0.38.0-preview.0.0.20241022154145-6a254edbec23 // indirect github.com/onflow/flow-nft/lib/go/contracts v1.2.2 // indirect github.com/onflow/flow-nft/lib/go/templates v1.2.1 // indirect github.com/onflow/flow/protobuf/go/flow v0.4.7 // indirect diff --git a/template_engine.go b/template_engine.go index e593f43..0fc27ae 100644 --- a/template_engine.go +++ b/template_engine.go @@ -4,6 +4,7 @@ import ( "bytes" "embed" "fmt" + "sync" "path" "strings" @@ -37,6 +38,7 @@ type ( client *Connector template *template.Template preloadedTemplates map[string]string + preloadedTemplatesMutex sync.Mutex wellKnownAddresses map[string]string wellKnownAddressesBinary map[string]flow.Address } @@ -91,12 +93,13 @@ func (e *TemplateEngine) loadContractAddresses(requiredWellKnownContracts []stri return fmt.Errorf("address not found for contract %s", requiredContractName) } } - log.Debug().Str("addresses", fmt.Sprintf("%v", e.wellKnownAddresses)).Msg("Loaded contract addresses") for name, addr := range e.wellKnownAddressesBinary { e.wellKnownAddresses[name] = addr.HexWithPrefix() } + log.Debug().Str("addresses", fmt.Sprintf("%v", e.wellKnownAddresses)).Msg("Loaded contract addresses") + return nil } @@ -117,7 +120,10 @@ func (e *TemplateEngine) GetStandardScript(scriptID string) string { } s = buf.String() + + e.preloadedTemplatesMutex.Lock() e.preloadedTemplates[scriptID] = s + e.preloadedTemplatesMutex.Unlock() } return s diff --git a/transaction.go b/transaction.go index 511553a..33ecf8d 100644 --- a/transaction.go +++ b/transaction.go @@ -287,11 +287,11 @@ func (tb FlowTransactionBuilder) Run(ctx context.Context) []flow.Event { tb.Connector.Logger.Error(fmt.Sprintf("Error executing script: %s output %v", tb.FileName, err)) os.Exit(1) } - return events + return events.Events } // RunE runs returns error -func (tb FlowTransactionBuilder) RunE(ctx context.Context) ([]flow.Event, error) { +func (tb FlowTransactionBuilder) RunE(ctx context.Context) (*flow.TransactionResult, error) { if tb.Proposer == nil { return nil, errors.New("you need to set the proposer") @@ -370,7 +370,7 @@ func (tb FlowTransactionBuilder) RunE(ctx context.Context) ([]flow.Event, error) } tb.Connector.Logger.Debug(fmt.Sprintf("Transaction %s successfully applied", tx.FlowTransaction().ID())) - return res.Events, nil + return res, nil } func (tb FlowTransactionBuilder) getContractCode(codeFileName string) ([]byte, error) {