Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/Stratis.Bitcoin.Features.Api/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Converters;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI;
Expand All @@ -28,6 +29,7 @@ public Startup(IWebHostEnvironment env, IFullNode fullNode)
}

private IFullNode fullNode;
private SwaggerUIOptions uiOptions;

public IConfigurationRoot Configuration { get; }

Expand Down Expand Up @@ -109,8 +111,15 @@ public void ConfigureServices(IServiceCollection services)
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();

// Register the Swagger generator. This will use the options we injected just above.
services.AddSwaggerGen();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("contracts", new OpenApiInfo { Title = "Contract API", Version = "1" });

});
services.AddSwaggerGenNewtonsoftSupport(); // Use Newtonsoft JSON serializer with swagger. Needs to be placed after AddSwaggerGen()

// Hack to be able to access and modify the options object
services.AddSingleton(_ => this.uiOptions);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down Expand Up @@ -141,6 +150,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}

// Hack to be able to access and modify the options object configured here
this.uiOptions = c;
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.OpenApi.Models;
using Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor;
using Stratis.SmartContracts;
using Stratis.SmartContracts.CLR.Compilation;
using Stratis.SmartContracts.CLR.Loader;
using Xunit;

namespace Stratis.Bitcoin.Features.SmartContracts.Tests
{
public class ContractSchemaFactoryTests
{
private const string Code = @"
using Stratis.SmartContracts;
[Deploy]
public class PrimitiveParams : SmartContract
{
public PrimitiveParams(ISmartContractState state): base(state) {}
public void AcceptsBool(bool b) {}
public void AcceptsByte(byte bb) {}
public void AcceptsByteArray(byte[] ba) {}
public void AcceptsChar(char c) {}
public void AcceptsString(string s) {}
public void AcceptsUint(uint ui) {}
public void AcceptsUlong(ulong ul) {}
public void AcceptsInt(int i) {}
public void AcceptsLong(long l) {}
public void AcceptsAddress(Address a) {}
public bool SomeProperty {get; set;}
public void AcceptsAllParams(bool b, byte bb, byte[] ba, char c, string s, uint ui, ulong ul, int i, long l, Address a) {}
}
public class DontDeploy : SmartContract
{
public DontDeploy(ISmartContractState state): base(state) {}
public void SomeMethod(string i) {}
}
";
[Fact]
public void Map_Parameter_Type_Success()
{
var compilationResult = ContractCompiler.Compile(Code);

var assembly = Assembly.Load(compilationResult.Compilation);

var mapper = new ContractSchemaFactory();

MethodInfo methodInfo = assembly.ExportedTypes.First(t => t.Name == "PrimitiveParams").GetMethod("AcceptsAllParams");
var schema = mapper.Map(methodInfo);
var properties = schema.Properties;

Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(bool)]().Type, properties["b"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(byte)]().Type, properties["bb"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(byte[])]().Type, properties["ba"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(char)]().Type, properties["c"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(string)]().Type, properties["s"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(uint)]().Type, properties["ui"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(ulong)]().Type, properties["ul"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(int)]().Type, properties["i"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(long)]().Type, properties["l"].Type);
Assert.Equal(ContractSchemaFactory.PrimitiveTypeMap[typeof(string)]().Type, properties["a"].Type);
}

[Fact]
public void Map_Type_Success()
{
var compilationResult = ContractCompiler.Compile(Code);

var assembly = Assembly.Load(compilationResult.Compilation);

var mapper = new ContractSchemaFactory();

// Maps the methods in a type to schemas.
IDictionary<string, OpenApiSchema> mapped = mapper.Map(new ContractAssembly(assembly).GetPublicMethods());

Assert.Equal("AcceptsBool", mapped["AcceptsBool"].Title);
Assert.Equal("AcceptsByte", mapped["AcceptsByte"].Title);
Assert.Equal("AcceptsByteArray", mapped["AcceptsByteArray"].Title);
Assert.Equal("AcceptsChar", mapped["AcceptsChar"].Title);
Assert.Equal("AcceptsString", mapped["AcceptsString"].Title);
Assert.Equal("AcceptsUint", mapped["AcceptsUint"].Title);
Assert.Equal("AcceptsUlong", mapped["AcceptsUlong"].Title);
Assert.Equal("AcceptsInt", mapped["AcceptsInt"].Title);
Assert.Equal("AcceptsLong", mapped["AcceptsLong"].Title);
Assert.Equal("AcceptsAddress", mapped["AcceptsAddress"].Title);

Assert.Equal(11, mapped.Count);
}

[Fact]
public void Only_Map_Deployed_Type_Success()
{
var compilationResult = ContractCompiler.Compile(Code);

var assembly = Assembly.Load(compilationResult.Compilation);

var mapper = new ContractSchemaFactory();

IDictionary<string, OpenApiSchema> mapped = mapper.Map(new ContractAssembly(assembly));

Assert.Equal(11, mapped.Count);
Assert.False(mapped.ContainsKey("SomeMethod"));
}

[Fact]
public void Only_Map_Deployed_Type_Single_Contract_Success()
{
string code = @"
using Stratis.SmartContracts;
public class PrimitiveParams : SmartContract
{
public PrimitiveParams(ISmartContractState state): base(state) {}
public void SomeMethod(string i) {}
}
";
var compilationResult = ContractCompiler.Compile(code);

var assembly = Assembly.Load(compilationResult.Compilation);

var contractAssembly = new ContractAssembly(assembly);

var mapper = new ContractSchemaFactory();

IDictionary<string, OpenApiSchema> mapped = mapper.Map(contractAssembly);

Assert.Equal(1, mapped.Count);
Assert.True(mapped.ContainsKey("SomeMethod"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Linq;
using Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor;
using Stratis.SmartContracts.CLR.Compilation;
using Stratis.SmartContracts.CLR.Serialization;
using Xunit;

namespace Stratis.Bitcoin.Features.SmartContracts.Tests
{
public class ParameterInfoMapperExtensionsTests
{
[Fact]
public void Map_Method_Params_Success()
{
var code = @"
using Stratis.SmartContracts;
public class Test
{
public void AcceptsAllParams(bool b, byte bb, byte[] ba, char c, string s, uint ui, ulong ul, int i, long l, Address a) {}
}
";
var compiled = ContractCompiler.Compile(code).Compilation;
var assembly = Assembly.Load(compiled);
var method = assembly.ExportedTypes.First().GetMethod("AcceptsAllParams");

// The jObject as we expect it to come from swagger.
var jObject = JObject.FromObject(new
{
b = "true",
bb = "DD",
ba = "AABB",
c = 'a',
s = "Test",
ui = 12,
ul = 123123128823,
i = 257,
l = 1238457438573495346,
a = "address"
});

var mapped = method.GetParameters().Map(jObject);

// Check the order and type of each param is correct.
Assert.Equal(10, mapped.Length);
Assert.Equal($"{(int)MethodParameterDataType.Bool}#true", mapped[0]);
Assert.Equal($"{(int)MethodParameterDataType.Byte}#DD", mapped[1]);
Assert.Equal($"{(int)MethodParameterDataType.ByteArray}#AABB", mapped[2]);
Assert.Equal($"{(int)MethodParameterDataType.Char}#a", mapped[3]);
Assert.Equal($"{(int)MethodParameterDataType.String}#Test", mapped[4]);
Assert.Equal($"{(int)MethodParameterDataType.UInt}#12", mapped[5]);
Assert.Equal($"{(int)MethodParameterDataType.ULong}#123123128823", mapped[6]);
Assert.Equal($"{(int)MethodParameterDataType.Int}#257", mapped[7]);
Assert.Equal($"{(int)MethodParameterDataType.Long}#1238457438573495346", mapped[8]);
Assert.Equal($"{(int)MethodParameterDataType.Address}#address", mapped[9]);

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Stratis.SmartContracts.CLR.Loader;

namespace Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor
{
public static class ContractAssemblyExtensions
{
/// <summary>
/// Gets the public methods defined by the contract, ignoring property getters/setters.
/// </summary>
/// <returns></returns>
public static IEnumerable<MethodInfo> GetPublicMethods(this IContractAssembly contractAssembly)
{
Type deployedType = contractAssembly.DeployedType;

if (deployedType == null)
return new List<MethodInfo>();

return deployedType
.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) // Get only the methods declared on the contract type
.Where(m => !m.IsSpecialName); // Ignore property setters/getters
}

public static IEnumerable<PropertyInfo> GetPublicGetterProperties(this IContractAssembly contractAssembly)
{
Type deployedType = contractAssembly.DeployedType;

if (deployedType == null)
return new List<PropertyInfo>();

return deployedType
.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetGetMethod() != null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.OpenApi.Models;
using Stratis.SmartContracts;
using Stratis.SmartContracts.CLR.Loader;

namespace Stratis.Bitcoin.Features.SmartContracts.ReflectionExecutor
{
/// <summary>
/// Factory for generating swagger schema for smart contract primitives.
/// </summary>
public class ContractSchemaFactory
{
public static readonly Dictionary<Type, Func<OpenApiSchema>> PrimitiveTypeMap = new Dictionary<Type, Func<OpenApiSchema>>
{
{ typeof(short), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(ushort), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(int), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(uint), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(long), () => new OpenApiSchema { Type = "integer", Format = "int64" } },
{ typeof(ulong), () => new OpenApiSchema { Type = "integer", Format = "int64" } },
{ typeof(float), () => new OpenApiSchema { Type = "number", Format = "float" } },
{ typeof(double), () => new OpenApiSchema { Type = "number", Format = "double" } },
{ typeof(decimal), () => new OpenApiSchema { Type = "number", Format = "double" } },
{ typeof(byte), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(sbyte), () => new OpenApiSchema { Type = "integer", Format = "int32" } },
{ typeof(byte[]), () => new OpenApiSchema { Type = "string", Format = "byte" } },
{ typeof(sbyte[]), () => new OpenApiSchema { Type = "string", Format = "byte" } },
{ typeof(char), () => new OpenApiSchema { Type = "string", Format = "char" } },
{ typeof(string), () => new OpenApiSchema { Type = "string" } },
{ typeof(bool), () => new OpenApiSchema { Type = "boolean" } },
{ typeof(Address), () => new OpenApiSchema { Type = "string" } }
};

/// <summary>
/// Maps a contract assembly to its schemas.
/// </summary>
/// <param name="assembly"></param>
/// <returns></returns>
public IDictionary<string, OpenApiSchema> Map(IContractAssembly assembly)
{
return this.Map(assembly.GetPublicMethods());
}

/// <summary>
/// Maps a type to its schemas.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public IDictionary<string, OpenApiSchema> Map(IEnumerable<MethodInfo> methods)
{
return methods.Select(this.Map).ToDictionary(k => k.Title, v => v);
}

/// <summary>
/// Maps a single method to a schema.
/// </summary>
/// <param name="method"></param>
/// <returns></returns>
public OpenApiSchema Map(MethodInfo method)
{
var schema = new OpenApiSchema();
schema.Properties = new Dictionary<string, OpenApiSchema>();
schema.Title = method.Name;

foreach (var parameter in method.GetParameters())
{
// Default to string.
OpenApiSchema paramSchema = PrimitiveTypeMap.ContainsKey(parameter.ParameterType)
? PrimitiveTypeMap[parameter.ParameterType]()
: PrimitiveTypeMap[typeof(string)]();

schema.Properties.Add(parameter.Name, paramSchema);
}

return schema;
}
}
}
Loading