From 4f81efb8a2700e061410f46cc3bdf055fdc930fe Mon Sep 17 00:00:00 2001 From: Jordan Andrews Date: Mon, 25 Nov 2019 17:01:02 +1100 Subject: [PATCH 1/3] Pretty return value for receipt --- .../PoW/SmartContractBlockDefinition.cs | 5 +- .../Controllers/SmartContractsController.cs | 10 ++- .../Rules/SmartContractCoinViewRuleLogic.cs | 21 ++++- .../PoW/SmartContractWalletTests.cs | 77 +++++++++++++++++++ .../SmartContracts/ReturnStruct.cs | 24 ++++++ ...tis.SmartContracts.IntegrationTests.csproj | 4 + .../StratisSmartContractNode.cs | 4 +- 7 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 src/Stratis.SmartContracts.IntegrationTests/SmartContracts/ReturnStruct.cs diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/PoW/SmartContractBlockDefinition.cs b/src/Stratis.Bitcoin.Features.SmartContracts/PoW/SmartContractBlockDefinition.cs index 83b76537f0e..8d33263c702 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/PoW/SmartContractBlockDefinition.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/PoW/SmartContractBlockDefinition.cs @@ -11,6 +11,7 @@ using Stratis.Bitcoin.Features.Miner; using Stratis.Bitcoin.Features.SmartContracts.Caching; using Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor.Consensus.Rules; +using Stratis.Bitcoin.Features.SmartContracts.Rules; using Stratis.Bitcoin.Mining; using Stratis.Bitcoin.Utilities; using Stratis.SmartContracts.CLR; @@ -233,6 +234,8 @@ private IContractExecutionResult ExecuteSmartContract(TxMempoolEntry mempoolEntr this.blockGasConsumed += result.GasConsumed; + string returnValue = SmartContractCoinViewRuleLogic.GetSerializedReturnValue(result.Return); + // Store all fields. We will reuse these in CoinviewRule. var receipt = new Receipt( new uint256(this.stateSnapshot.Root), @@ -243,7 +246,7 @@ private IContractExecutionResult ExecuteSmartContract(TxMempoolEntry mempoolEntr result.To, result.NewContractAddress, !result.Revert, - result.Return?.ToString(), + returnValue, result.ErrorMessage, deserializedCallData.Value.GasPrice, transactionContext.TxOutValue, diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs b/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs index d9a28bad376..260fc675dfc 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs @@ -17,7 +17,6 @@ using Stratis.Bitcoin.Features.Wallet.Broadcasting; using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Interfaces; -using Stratis.Bitcoin.Utilities; using Stratis.Bitcoin.Utilities.JsonErrors; using Stratis.Bitcoin.Utilities.ModelStateErrors; using Stratis.SmartContracts; @@ -582,9 +581,9 @@ public IActionResult LocalCallSmartContractTransaction([FromBody] LocalCallContr ContractTxData txData = this.smartContractTransactionService.BuildLocalCallTxData(request); ILocalExecutionResult result = this.localExecutor.Execute( - (ulong)this.chainIndexer.Height, + (ulong) this.chainIndexer.Height, request.Sender?.ToUint160(this.network) ?? new uint160(), - string.IsNullOrWhiteSpace(request.Amount) ? (Money)request.Amount : 0, + string.IsNullOrWhiteSpace(request.Amount) ? (Money) request.Amount : 0, txData); return this.Json(result, new JsonSerializerSettings @@ -597,6 +596,11 @@ public IActionResult LocalCallSmartContractTransaction([FromBody] LocalCallContr return this.Json(ErrorHelpers.BuildErrorResponse(HttpStatusCode.InternalServerError, e.Message, "Error deserializing method parameters")); } + catch (Exception e) + { + Console.WriteLine(e); + throw e; + } } /// diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/Rules/SmartContractCoinViewRuleLogic.cs b/src/Stratis.Bitcoin.Features.SmartContracts/Rules/SmartContractCoinViewRuleLogic.cs index 89ba6776964..84275c96b13 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/Rules/SmartContractCoinViewRuleLogic.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/Rules/SmartContractCoinViewRuleLogic.cs @@ -5,6 +5,7 @@ using CSharpFunctionalExtensions; using Microsoft.Extensions.Logging; using NBitcoin; +using Newtonsoft.Json; using Stratis.Bitcoin.Base.Deployments; using Stratis.Bitcoin.Consensus; using Stratis.Bitcoin.Consensus.Rules; @@ -218,6 +219,8 @@ public void ExecuteContractTransaction(RuleContext context, Transaction transact IContractExecutionResult result = executor.Execute(txContext); + string returnValue = GetSerializedReturnValue(result.Return); + var receipt = new Receipt( new uint256(this.mutableStateRepository.Root), result.GasConsumed, @@ -227,7 +230,7 @@ public void ExecuteContractTransaction(RuleContext context, Transaction transact result.To, result.NewContractAddress, !result.Revert, - result.Return?.ToString(), + returnValue, result.ErrorMessage, deserializedCallData.Value.GasPrice, txContext.TxOutValue, @@ -252,6 +255,22 @@ public void ExecuteContractTransaction(RuleContext context, Transaction transact this.CheckBlockGasLimit(result.GasConsumed); } + /// + /// Get a string representation of a smart contract execution's return value. + /// + public static string GetSerializedReturnValue(object returnValue) + { + if (returnValue == null) + return null; + + // A primitive. Return the standard string representation. + if (returnValue.GetType().Assembly.FullName == "System") + return returnValue.ToString(); + + // A custom type. Get a JSON representation. + return JsonConvert.SerializeObject(returnValue); + } + /// /// Update the total gas expenditure for this block and verify that it has not exceeded the limit. /// diff --git a/src/Stratis.SmartContracts.IntegrationTests/PoW/SmartContractWalletTests.cs b/src/Stratis.SmartContracts.IntegrationTests/PoW/SmartContractWalletTests.cs index fabacb608fe..aa682b7a53f 100644 --- a/src/Stratis.SmartContracts.IntegrationTests/PoW/SmartContractWalletTests.cs +++ b/src/Stratis.SmartContracts.IntegrationTests/PoW/SmartContractWalletTests.cs @@ -4,9 +4,14 @@ using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; +using Flurl; +using Flurl.Http; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitcoin.Networks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Stratis.Bitcoin.Features.SmartContracts; using Stratis.Bitcoin.Features.SmartContracts.Models; using Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor.Consensus.Rules; @@ -418,6 +423,78 @@ public void MockChain_NonFungibleToken() } } + [Fact] + public async Task Struct_ReturnValue() + { + using (PoWMockChain chain = new PoWMockChain(2)) + { + MockChainNode sender = chain.Nodes[0]; + + // Mine some coins so we have balance + int maturity = (int)sender.CoreNode.FullNode.Network.Consensus.CoinbaseMaturity; + sender.MineBlocks(maturity + 1); + int spendable = GetSpendableBlocks(maturity + 1, maturity); + Assert.Equal(Money.COIN * spendable * 50, (long)sender.WalletSpendableBalance); + + // Compile file + ContractCompilationResult compilationResult = ContractCompiler.CompileFile("SmartContracts/ReturnStruct.cs"); + Assert.True(compilationResult.Success); + + // Create contract + BuildCreateContractTransactionResponse createResponse = sender.SendCreateContractTransaction(compilationResult.Compilation, 30); + Assert.True(createResponse.Success); + sender.WaitMempoolCount(1); + sender.MineBlocks(1); + ReceiptResponse createReceipt = sender.GetReceipt(createResponse.TransactionId.ToString()); + Assert.True(createReceipt.Success); + + // Get the representation we expect to see + var theStruct = new ReturnStruct.TheStruct + { + TestBool = true, + TestString = "My String" + }; + string jsonStruct = JsonConvert.SerializeObject(theStruct); + + // Make a local call and check the result matches. + var localCallResult = await $"http://localhost:{sender.CoreNode.ApiPort}/api" + .AppendPathSegment("SmartContracts/local-call") + .PostJsonAsync(new LocalCallContractRequest + { + Amount = "0", + ContractAddress = createResponse.NewContractAddress, + GasLimit = 100_000, + GasPrice = 100, + MethodName = "CallMe", + Sender = sender.MinerAddress.Address + }); + + string content = await localCallResult.Content.ReadAsStringAsync(); + JObject fullExecutionResult = JObject.Parse(content); + JObject jsonReturnValue = (JObject) fullExecutionResult["return"]; + + Assert.Equal(CleanJson(jsonStruct), CleanJson(jsonReturnValue.ToString())); + + // Call contract for real + BuildCallContractTransactionResponse callResponse = sender.SendCallContractTransaction("CallMe", createResponse.NewContractAddress, 0); + Assert.True(callResponse.Success); + sender.WaitMempoolCount(1); + sender.MineBlocks(1); + + // Check the receipt + var receipt = sender.GetReceipt(callResponse.TransactionId.ToString()); + Assert.Equal(CleanJson(jsonStruct), CleanJson(receipt.ReturnValue)); + } + } + + private string CleanJson(string json) + { + return json.Replace("/", "") + .Replace(" ", "") + .Replace("\r\n", "") + .ToLower(); + } + [Fact] public void Create_WithFunds() { diff --git a/src/Stratis.SmartContracts.IntegrationTests/SmartContracts/ReturnStruct.cs b/src/Stratis.SmartContracts.IntegrationTests/SmartContracts/ReturnStruct.cs new file mode 100644 index 00000000000..41ae701ae5d --- /dev/null +++ b/src/Stratis.SmartContracts.IntegrationTests/SmartContracts/ReturnStruct.cs @@ -0,0 +1,24 @@ + +using Stratis.SmartContracts; + +public class ReturnStruct : SmartContract +{ + public ReturnStruct(ISmartContractState state) : base(state) + { + } + + public TheStruct CallMe() + { + return new TheStruct + { + TestBool = true, + TestString = "My String" + }; + } + + public struct TheStruct + { + public string TestString; + public bool TestBool; + } +} diff --git a/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj b/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj index e20aefcadde..d1e8fcd10c5 100644 --- a/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj +++ b/src/Stratis.SmartContracts.IntegrationTests/Stratis.SmartContracts.IntegrationTests.csproj @@ -36,6 +36,7 @@ + @@ -150,6 +151,9 @@ PreserveNewest + + PreserveNewest + Always diff --git a/src/Stratis.SmartContracts.Tests.Common/StratisSmartContractNode.cs b/src/Stratis.SmartContracts.Tests.Common/StratisSmartContractNode.cs index cadf1aa0274..5ae3c762559 100644 --- a/src/Stratis.SmartContracts.Tests.Common/StratisSmartContractNode.cs +++ b/src/Stratis.SmartContracts.Tests.Common/StratisSmartContractNode.cs @@ -3,6 +3,7 @@ using Stratis.Bitcoin.Base; using Stratis.Bitcoin.Builder; using Stratis.Bitcoin.Configuration; +using Stratis.Bitcoin.Features.Api; using Stratis.Bitcoin.Features.BlockStore; using Stratis.Bitcoin.Features.MemoryPool; using Stratis.Bitcoin.Features.RPC; @@ -41,7 +42,8 @@ public override void BuildNode() .UseSmartContractWallet() .UseSmartContractPowMining() .MockIBD() - .UseTestChainedHeaderTree(); + .UseTestChainedHeaderTree() + .UseApi(); if (!this.EnablePeerDiscovery) { From 45dafc456b760ef5c76227171bbafa2c98f22408 Mon Sep 17 00:00:00 2001 From: Jordan Andrews Date: Mon, 25 Nov 2019 17:07:53 +1100 Subject: [PATCH 2/3] Slight change of approach --- .../Rules/SmartContractCoinViewRuleLogic.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/Rules/SmartContractCoinViewRuleLogic.cs b/src/Stratis.Bitcoin.Features.SmartContracts/Rules/SmartContractCoinViewRuleLogic.cs index 84275c96b13..531aad16022 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/Rules/SmartContractCoinViewRuleLogic.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/Rules/SmartContractCoinViewRuleLogic.cs @@ -263,11 +263,11 @@ public static string GetSerializedReturnValue(object returnValue) if (returnValue == null) return null; - // A primitive. Return the standard string representation. - if (returnValue.GetType().Assembly.FullName == "System") + // All primitives will come from "a real place". Return the standard string representation. + if (!String.IsNullOrEmpty(returnValue.GetType().Namespace)) return returnValue.ToString(); - // A custom type. Get a JSON representation. + // A custom type. From an empty namespace. Get a JSON representation. return JsonConvert.SerializeObject(returnValue); } From 5e5371508c5109b848fbe39d41cebed930f78bc9 Mon Sep 17 00:00:00 2001 From: Jordan Andrews Date: Mon, 25 Nov 2019 17:11:29 +1100 Subject: [PATCH 3/3] Undo --- .../Controllers/SmartContractsController.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs b/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs index 260fc675dfc..d9a28bad376 100644 --- a/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs +++ b/src/Stratis.Bitcoin.Features.SmartContracts/ReflectionExecutor/Controllers/SmartContractsController.cs @@ -17,6 +17,7 @@ using Stratis.Bitcoin.Features.Wallet.Broadcasting; using Stratis.Bitcoin.Features.Wallet.Interfaces; using Stratis.Bitcoin.Interfaces; +using Stratis.Bitcoin.Utilities; using Stratis.Bitcoin.Utilities.JsonErrors; using Stratis.Bitcoin.Utilities.ModelStateErrors; using Stratis.SmartContracts; @@ -581,9 +582,9 @@ public IActionResult LocalCallSmartContractTransaction([FromBody] LocalCallContr ContractTxData txData = this.smartContractTransactionService.BuildLocalCallTxData(request); ILocalExecutionResult result = this.localExecutor.Execute( - (ulong) this.chainIndexer.Height, + (ulong)this.chainIndexer.Height, request.Sender?.ToUint160(this.network) ?? new uint160(), - string.IsNullOrWhiteSpace(request.Amount) ? (Money) request.Amount : 0, + string.IsNullOrWhiteSpace(request.Amount) ? (Money)request.Amount : 0, txData); return this.Json(result, new JsonSerializerSettings @@ -596,11 +597,6 @@ public IActionResult LocalCallSmartContractTransaction([FromBody] LocalCallContr return this.Json(ErrorHelpers.BuildErrorResponse(HttpStatusCode.InternalServerError, e.Message, "Error deserializing method parameters")); } - catch (Exception e) - { - Console.WriteLine(e); - throw e; - } } ///