static bool TryGetDeploymentSigner(ConfigProps config, ExpressChain?chain, byte version, [MaybeNullWhen(false)] out Signer signer)
            {
                if (config.TryGetValue("deploy-signer", out var deploySignerToken))
                {
                    return(TryParseSigner(deploySignerToken, chain, version, out signer));
                }

                signer = default;
                return(false);
            }
        static async Task <IApplicationEngine> CreateEngineAsync(ConfigProps config)
        {
            if (!config.TryGetValue("invocation", out var jsonInvocation))
            {
                throw new JsonException("missing invocation property");
            }

            if (jsonInvocation.Type == JTokenType.Object && jsonInvocation["trace-file"] != null)
            {
                var traceFile      = jsonInvocation.Value <string>("trace-file") ?? throw new JsonException("invalid trace-file property");
                var program        = ParseProgram(config);
                var launchContract = LoadNefFile(program);
                var contracts      = new List <NefFile> {
                    launchContract
                };

                // TODO: load other contracts?

                return(new TraceApplicationEngine(traceFile, contracts));
            }

            return(await CreateDebugEngineAsync(config, jsonInvocation).ConfigureAwait(false));
        }
        static async Task <IApplicationEngine> CreateDebugEngineAsync(ConfigProps config, JToken jsonInvocation)
        {
            var program        = ParseProgram(config);
            var launchNefFile  = LoadNefFile(program);
            var launchManifest = await LoadContractManifestAsync(program).ConfigureAwait(false);

            var chain      = LoadNeoExpress(config);
            var invocation = ParseInvocation(jsonInvocation);

            var checkpoint = LoadBlockchainCheckpoint(config, chain?.Network, chain?.AddressVersion);

            var(trigger, witnessChecker) = ParseRuntime(config, chain, checkpoint.Settings.AddressVersion);
            if (trigger != TriggerType.Application)
            {
                throw new Exception($"Trigger Type {trigger} not supported");
            }

            var signers = ParseSigners(config, chain, checkpoint.Settings.AddressVersion).ToArray();

            var store = new MemoryTrackingStore(checkpoint);

            store.EnsureLedgerInitialized(checkpoint.Settings);

            Script invokeScript;
            var    attributes = Array.Empty <TransactionAttribute>();

            if (invocation.IsT3) // T3 == ContractDeploymentInvocation
            {
                if ((signers.Length == 0 || (signers.Length == 1 && signers[0].Account == UInt160.Zero)) &&
                    TryGetDeploymentSigner(config, chain, checkpoint.Settings.AddressVersion, out var deploySigner))
                {
                    signers = new[] { deploySigner };
                }

                using var builder = new ScriptBuilder();
                builder.EmitDynamicCall(NativeContract.ContractManagement.Hash, "deploy", launchNefFile.ToArray(), launchManifest.ToJson().ToString());
                invokeScript = builder.ToArray();
            }
            else
            {
                var paramParser  = CreateContractParameterParser(checkpoint.Settings.AddressVersion, store, chain);
                var deploySigner = TryGetDeploymentSigner(config, chain, checkpoint.Settings.AddressVersion, out var _deploySigner)
                    ? _deploySigner
                    : new Signer {
                    Account = UInt160.Zero
                };

                var(launchContractId, launchContractHash) = EnsureContractDeployed(store, launchNefFile, launchManifest, deploySigner, checkpoint.Settings);
                UpdateContractStorage(store, launchContractId, ParseStorage(config, paramParser));
                invokeScript = await CreateInvokeScriptAsync(invocation, program, launchContractHash, paramParser);

                if (invocation.IsT1) // T1 == OracleResponseInvocation
                {
                    attributes = GetTransactionAttributes(invocation.AsT1, store, launchContractHash, paramParser);
                }
            }

            // TODO: load other contracts
            //          Not sure supporting other contracts is a good idea anymore. Since there's no way to calculate the
            //          contract id hash prior to deployment in Neo 3, I'm thinking the better approach would be to simply
            //          deploy whatever contracts you want and take a snapshot rather than deploying multiple contracts
            //          during launch configuration.

            var tx = new Transaction
            {
                Version         = 0,
                Nonce           = (uint)new Random().Next(),
                Script          = invokeScript,
                Signers         = signers,
                ValidUntilBlock = checkpoint.Settings.MaxValidUntilBlockIncrement,
                Attributes      = attributes,
                Witnesses       = Array.Empty <Witness>()
            };

            var block  = CreateDummyBlock(store, tx);
            var engine = new DebugApplicationEngine(tx, store, checkpoint.Settings, block, witnessChecker);

            engine.LoadScript(invokeScript);
            return(engine);
 static string ParseProgram(ConfigProps config) => config["program"].Value <string>() ?? throw new JsonException("missing program property");