diff --git a/README.md b/README.md index 6c7d3f51..2244ad5d 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ values. | `--computation-reporting` | `FLOW_COMPUTATIONREPORTING` | `false` | Enable computation reporting for Cadence scripts & transactions | | `--checkpoint-dir` | `FLOW_CHECKPOINTDIR` | '' | Checkpoint directory to load the emulator state from, if starting the emulator from a checkpoint | | `--state-hash` | `FLOW_STATEHASH` | '' | State hash of the checkpoint, if starting the emulator from a checkpoint | +| `--num-accounts` | `FLOW_NUMACCOUNTS` | `0` | Precreate and fund this many accounts at startup (mints 1000.0 FLOW to each) | ## Running the emulator with the Flow CLI diff --git a/cmd/emulator/start/start.go b/cmd/emulator/start/start.go index e25faef4..7f47204c 100644 --- a/cmd/emulator/start/start.go +++ b/cmd/emulator/start/start.go @@ -83,6 +83,7 @@ type Config struct { ScheduledTransactionsEnabled bool `default:"true" flag:"scheduled-transactions" info:"enable Cadence scheduled transactions"` SetupEVMEnabled bool `default:"true" flag:"setup-evm" info:"enable EVM setup for the emulator, this will deploy the EVM contracts"` SetupVMBridgeEnabled bool `default:"true" flag:"setup-vm-bridge" info:"enable VM Bridge setup for the emulator, this will deploy the VM Bridge contracts"` + NumAccounts int `default:"0" flag:"num-accounts" info:"number of precreated accounts at startup"` } const EnvPrefix = "FLOW" @@ -225,6 +226,7 @@ func Cmd(config StartConfig) *cobra.Command { ScheduledTransactionsEnabled: conf.ScheduledTransactionsEnabled, SetupEVMEnabled: conf.SetupEVMEnabled, SetupVMBridgeEnabled: conf.SetupVMBridgeEnabled, + NumAccounts: conf.NumAccounts, } emu := server.NewEmulatorServer(logger, serverConf) diff --git a/docs/overview.md b/docs/overview.md index 547a66f1..9737e581 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -75,6 +75,7 @@ values. | `--redis-url` | `FLOW_REDIS_URL` | '' | Redis-server URL for persisting redis storage backend ( `redis://[[username:]password@]host[:port][/database]` ) | | `--start-block-height` | `FLOW_STARTBLOCKHEIGHT` | `0` | Start block height to use when starting the network using 'testnet' or 'mainnet' as the chain-id | | `--rpc-host` | `FLOW_RPCHOST` | '' | RPC host (access node) to query for previous state when starting the network using 'testnet' or 'mainnet' as the chain-id | +| `--num-accounts` | `FLOW_NUMACCOUNTS` | `0` | Precreate and fund this many accounts at startup (mints 1000.0 FLOW to each) | ## Running the emulator with the Flow CLI diff --git a/server/server.go b/server/server.go index e30ba4cb..3fae8fb3 100644 --- a/server/server.go +++ b/server/server.go @@ -28,13 +28,17 @@ import ( "github.com/onflow/cadence" "github.com/onflow/cadence/runtime" + "github.com/onflow/cadence/stdlib" + flowsdk "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go-sdk/crypto" + "github.com/onflow/flow-go-sdk/templates" "github.com/onflow/flow-go/fvm/systemcontracts" flowgo "github.com/onflow/flow-go/model/flow" "github.com/psiemens/graceland" "github.com/rs/zerolog" "github.com/onflow/flow-emulator/adapters" + "github.com/onflow/flow-emulator/convert" "github.com/onflow/flow-emulator/emulator" "github.com/onflow/flow-emulator/server/access" "github.com/onflow/flow-emulator/server/debugger" @@ -155,6 +159,8 @@ type Config struct { SetupEVMEnabled bool // SetupVMBridgeEnabled enables the VM bridge setup for the emulator, defaults to true. SetupVMBridgeEnabled bool + // NumAccounts specifies how many accounts to precreate and fund at startup. + NumAccounts int } type listener interface { @@ -210,6 +216,161 @@ func NewEmulatorServer(logger *zerolog.Logger, conf *Config) *EmulatorServer { } } + // Precreate accounts if requested + if conf.NumAccounts > 0 { + // Funding amount (fixed, not configurable) + fundAmount := "1000.0" + + // Fetch system contract addresses + env := systemcontracts.SystemContractsForChain(chain.ChainID()).AsTemplateEnv() + + logger.Info().Msg("\nAvailable Accounts\n==================") + + serviceKey := emulatedBlockchain.ServiceKey() + servicePrivHex := fmt.Sprintf("0x%X", serviceKey.PrivateKey.Encode()) + + for i := 0; i < conf.NumAccounts; i++ { + // Create account using the same key as the service account + latestBlock, err := emulatedBlockchain.GetLatestBlock() + if err != nil { + logger.Error().Err(err).Msg("❗ Failed to get latest block for account creation") + continue + } + createTx, err := templates.CreateAccount( + []*flowsdk.AccountKey{serviceKey.AccountKey()}, + nil, + serviceKey.Address, + ) + if err != nil { + logger.Error().Err(err).Msg("❗ Failed to build account creation transaction") + continue + } + createTx. + SetComputeLimit(flowgo.DefaultMaxTransactionGasLimit). + SetReferenceBlockID(flowsdk.Identifier(latestBlock.ID())). + SetProposalKey(serviceKey.Address, serviceKey.Index, serviceKey.SequenceNumber). + SetPayer(serviceKey.Address) + + signer, err := serviceKey.Signer() + if err != nil { + logger.Error().Err(err).Msg("❗ Failed to get service key signer for account creation") + continue + } + if err := createTx.SignEnvelope(serviceKey.Address, serviceKey.Index, signer); err != nil { + logger.Error().Err(err).Msg("❗ Failed to sign account creation transaction") + continue + } + + if err := emulatedBlockchain.AddTransaction(*convert.SDKTransactionToFlow(*createTx)); err != nil { + logger.Error().Err(err).Msg("❗ Failed to submit account creation transaction") + continue + } + _, results, err := emulatedBlockchain.ExecuteAndCommitBlock() + if err != nil || len(results) == 0 || !results[len(results)-1].Succeeded() { + logger.Error().Err(err).Msg("❗ Failed to execute account creation transaction") + continue + } + if _, err := emulatedBlockchain.CommitBlock(); err != nil { + logger.Error().Err(err).Msg("❗ Failed to commit block after account creation") + continue + } + + // Extract new account address from events + var newAddr flowsdk.Address + last := results[len(results)-1] + for _, ev := range last.Events { + if ev.Type == flowsdk.EventAccountCreated { + addrFieldValue := cadence.SearchFieldByName(ev.Value, stdlib.AccountEventAddressParameter.Identifier) + if cadenceAddr, ok := addrFieldValue.(cadence.Address); ok { + newAddr = flowsdk.Address(cadenceAddr) + break + } + } + } + if newAddr == (flowsdk.Address{}) { + logger.Error().Msg("❗ Could not determine new account address from events") + continue + } + + // Fund account by minting and depositing FLOW from service account + txCode := fmt.Sprintf(` + import FungibleToken from %[1]s + import FlowToken from %[2]s + + transaction(recipient: Address, amount: UFix64) { + prepare(acct: auth(Storage, Capabilities, FungibleToken.Withdraw) &Account) { + let adminRef = acct.storage.borrow<&FlowToken.Administrator>(from: /storage/flowTokenAdmin) + ?? panic("missing FlowToken admin") + let minter <- adminRef.createNewMinter(allowedAmount: amount) + let minted <- minter.mintTokens(amount: amount) + let receiver = getAccount(recipient).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)! + .borrow() + ?? panic("missing FlowToken receiver") + receiver.deposit(from: <-minted) + destroy minter + } + } + `, env.FungibleTokenAddress, env.FlowTokenAddress) + + latestBlock, err = emulatedBlockchain.GetLatestBlock() + if err != nil { + logger.Error().Err(err).Msg("❗ Failed to get latest block for funding") + continue + } + fundVal, err := cadence.NewUFix64(fundAmount) + if err != nil { + logger.Error().Err(err).Msg("❗ Failed to parse funding amount") + continue + } + fundTx := flowsdk.NewTransaction(). + SetScript([]byte(txCode)). + SetReferenceBlockID(flowsdk.Identifier(latestBlock.ID())). + SetComputeLimit(flowgo.DefaultMaxTransactionGasLimit). + SetProposalKey(serviceKey.Address, serviceKey.Index, serviceKey.SequenceNumber). + SetPayer(serviceKey.Address) + + if err := fundTx.AddArgument(cadence.NewAddress(newAddr)); err != nil { + logger.Error().Err(err).Msg("❗ Failed to add recipient argument") + continue + } + if err := fundTx.AddArgument(fundVal); err != nil { + logger.Error().Err(err).Msg("❗ Failed to add amount argument") + continue + } + + signer, err = serviceKey.Signer() + if err != nil { + logger.Error().Err(err).Msg("❗ Failed to get service key signer for funding") + continue + } + if err := fundTx.SignEnvelope(serviceKey.Address, serviceKey.Index, signer); err != nil { + logger.Error().Err(err).Msg("❗ Failed to sign funding transaction") + continue + } + + if err := emulatedBlockchain.AddTransaction(*convert.SDKTransactionToFlow(*fundTx)); err != nil { + logger.Error().Err(err).Msg("❗ Failed to submit funding transaction") + continue + } + _, results, err = emulatedBlockchain.ExecuteAndCommitBlock() + if err != nil || len(results) == 0 || !results[len(results)-1].Succeeded() { + logger.Error().Err(err).Msg("❗ Failed to execute funding transaction") + continue + } + if _, err := emulatedBlockchain.CommitBlock(); err != nil { + logger.Error().Err(err).Msg("❗ Failed to commit block after funding") + continue + } + + logger.Info().Msgf("(%d) 0x%s (%.18s FLOW)", i, newAddr.Hex(), fundAmount) + } + + logger.Info().Msg("\nPrivate Keys\n==================") + for i := 0; i < conf.NumAccounts; i++ { + logger.Info().Msgf("(%d) %s", i, servicePrivHex) + } + } + accessAdapter := adapters.NewAccessAdapter(logger, emulatedBlockchain) livenessTicker := utils.NewLivenessTicker(conf.LivenessCheckTolerance) grpcServer := access.NewGRPCServer(logger, emulatedBlockchain, accessAdapter, chain, conf.Host, conf.GRPCPort, conf.GRPCDebug) diff --git a/server/server_test.go b/server/server_test.go index 76fe04cb..961bccff 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -26,6 +26,7 @@ import ( "time" "github.com/onflow/cadence" + "github.com/onflow/cadence/stdlib" "github.com/onflow/flow-emulator/convert" flowsdk "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go-sdk/templates" @@ -322,3 +323,53 @@ func TestScheduledCallback_IncrementsCounter(t *testing.T) { require.NoError(t, res.Error) require.Equal(t, cadence.NewInt(1), res.Value) } + +func TestPrecreateAccounts_KeysMatchServiceKey(t *testing.T) { + logger := zerolog.Nop() + conf := &Config{NumAccounts: 3} + server := NewEmulatorServer(&logger, conf) + require.NotNil(t, server) + defer server.Stop() + + serviceKey := server.Emulator().ServiceKey() + servicePub := serviceKey.AccountKey().PublicKey.Encode() + serviceAddr := serviceKey.Address + + latest, err := server.Emulator().GetLatestBlock() + require.NoError(t, err) + + blockEvents, err := server.Emulator().GetEventsForHeightRange(flowsdk.EventAccountCreated, 0, latest.Height) + require.NoError(t, err) + + var createdAddrs []flowsdk.Address + for _, be := range blockEvents { + // Convert flow-go events to SDK events to inspect the Cadence payload + sdkEvents, err := convert.FlowEventsToSDK(be.Events) + require.NoError(t, err) + for _, ev := range sdkEvents { + if ev.Type != flowsdk.EventAccountCreated { + continue + } + addrFieldValue := cadence.SearchFieldByName(ev.Value, stdlib.AccountEventAddressParameter.Identifier) + cadAddr, ok := addrFieldValue.(cadence.Address) + if !ok { + continue + } + addr := flowsdk.Address(cadAddr) + if addr == serviceAddr { + continue + } + createdAddrs = append(createdAddrs, addr) + } + } + + // Validate created accounts have the expected key + for _, addr := range createdAddrs { + flowAddr := convert.SDKAddressToFlow(addr) + acct, err := server.Emulator().GetAccount(flowAddr) + require.NoError(t, err) + require.NotEmpty(t, acct.Keys) + gotPub := acct.Keys[0].PublicKey.Encode() + require.Equal(t, servicePub, gotPub) + } +}