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/Rules/SmartContractCoinViewRuleLogic.cs b/src/Stratis.Bitcoin.Features.SmartContracts/Rules/SmartContractCoinViewRuleLogic.cs index 89ba6776964..531aad16022 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; + + // 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. From an empty namespace. 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) {