diff --git a/Testnet/MintableTokenInvoice/.gitignore b/Testnet/MintableTokenInvoice/.gitignore new file mode 100644 index 00000000..1b9a752d --- /dev/null +++ b/Testnet/MintableTokenInvoice/.gitignore @@ -0,0 +1,329 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ \ No newline at end of file diff --git a/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/BaseContractTest.cs b/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/BaseContractTest.cs new file mode 100644 index 00000000..31f560a8 --- /dev/null +++ b/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/BaseContractTest.cs @@ -0,0 +1,87 @@ +namespace MintableTokenInvoiceTests; + +using System; +using Moq; +using Stratis.SmartContracts; +using Stratis.SmartContracts.CLR; +using Stratis.SmartContracts.CLR.Serialization; + +public class BaseContractTest +{ + protected Mock MockContractState { get; private set; } + + protected Mock MockContractLogger { get; private set; } + + protected Mock MockInternalExecutor { get; private set; } + + protected InMemoryState PersistentState { get; private set; } + + protected ISerializer Serializer { get; private set; } + + protected Address Contract { get; private set; } + + protected Address Owner { get; private set; } + + protected Address AddressOne { get; private set; } + + protected Address AddressTwo { get; private set; } + + protected Address AddressThree { get; private set; } + + protected Address AddressFour { get; private set; } + + protected Address AddressFive { get; private set; } + + protected Address AddressSix { get; private set; } + + protected Address IdentityContract { get; private set; } + + protected BaseContractTest() + { + this.Serializer = new Serializer(new ContractPrimitiveSerializerV2(null)); // new SmartContractsPoARegTest() + this.PersistentState = new InMemoryState(); + this.MockContractLogger = new Mock(); + this.MockContractState = new Mock(); + this.MockInternalExecutor = new Mock(); + this.MockContractState.Setup(x => x.PersistentState).Returns(this.PersistentState); + this.MockContractState.Setup(x => x.ContractLogger).Returns(this.MockContractLogger.Object); + this.MockContractState.Setup(x => x.InternalTransactionExecutor).Returns(this.MockInternalExecutor.Object); + this.MockContractState.Setup(x => x.Serializer).Returns(this.Serializer); + this.Contract = "0x0000000000000000000000000000000000000001".HexToAddress(); + this.Owner = "0x0000000000000000000000000000000000000002".HexToAddress(); + this.AddressOne = "0x0000000000000000000000000000000000000003".HexToAddress(); + this.AddressTwo = "0x0000000000000000000000000000000000000004".HexToAddress(); + this.AddressThree = "0x0000000000000000000000000000000000000005".HexToAddress(); + this.AddressFour = "0x0000000000000000000000000000000000000006".HexToAddress(); + this.AddressFive = "0x0000000000000000000000000000000000000007".HexToAddress(); + this.AddressSix = "0x0000000000000000000000000000000000000008".HexToAddress(); + this.IdentityContract = "0x000000000000000000000000000000000000000F".HexToAddress(); + } + + protected MintableTokenInvoice CreateNewMintableTokenContract() + { + this.MockContractState.Setup(x => x.Message).Returns(new Message(this.Contract, this.Owner, 0)); + this.MockContractState.Setup(x => x.InternalHashHelper).Returns(new InternalHashHelper()); + + var addresses = new[] { this.AddressOne, this.AddressTwo, this.AddressThree }; + var bytes = this.Serializer.Serialize(addresses); + + return new MintableTokenInvoice(this.MockContractState.Object, 1000, this.IdentityContract); + } + + protected void SetupMessage(Address contractAddress, Address sender, ulong value = 0) + { + this.MockContractState.Setup(x => x.Message).Returns(new Message(contractAddress, sender, value)); + } + + protected void SetupBlock(ulong blockNumber) + { + this.MockContractState.Setup(x => x.Block.Number).Returns(blockNumber); + } + + protected void VerifyLog(T expectedLog, Func times) + where T : struct + { + this.MockContractLogger.Verify(x => x.Log(this.MockContractState.Object, expectedLog), times); + } +} diff --git a/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/InMemoryState.cs b/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/InMemoryState.cs new file mode 100644 index 00000000..4eb15c45 --- /dev/null +++ b/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/InMemoryState.cs @@ -0,0 +1,76 @@ +using NBitcoin; +using Stratis.SmartContracts; +using System; +using System.Collections.Generic; + +namespace MintableTokenInvoiceTests +{ + public class InMemoryState : IPersistentState + { + private readonly Dictionary _storage = new Dictionary(); + + public bool IsContractResult { get; set; } + + public void Clear(string key) => _storage.Remove(key); + + public T GetValue(string key) => (T)_storage.GetValueOrDefault(key, default(T)); + + public Address GetAddress(string key) => GetValue
(key); + + public T[] GetArray(string key) => GetValue(key); + + public bool GetBool(string key) => GetValue(key); + + public byte[] GetBytes(byte[] key) => throw new NotImplementedException(); + + public byte[] GetBytes(string key) => GetValue(key); + + public char GetChar(string key) => GetValue(key); + + public int GetInt32(string key) => GetValue(key); + + public long GetInt64(string key) => GetValue(key); + + public UInt256 GetUInt256(string key) => GetValue(key); + + public string GetString(string key) => GetValue(key); + + public T GetStruct(string key) where T : struct => GetValue(key); + + public uint GetUInt32(string key) => GetValue(key); + + public ulong GetUInt64(string key) => GetValue(key); + + public UInt128 GetUInt128(string key) => GetValue(key); + + public bool IsContract(Address address) => IsContractResult; + + public void SetAddress(string key, Address value) => _storage.AddOrReplace(key, value); + + public void SetArray(string key, Array a) => _storage.AddOrReplace(key, a); + + public void SetBool(string key, bool value) => _storage.AddOrReplace(key, value); + + public void SetBytes(byte[] key, byte[] value) => throw new NotImplementedException(); + + public void SetBytes(string key, byte[] value) => _storage.AddOrReplace(key, value); + + public void SetChar(string key, char value) => _storage.AddOrReplace(key, value); + + public void SetInt32(string key, int value) => _storage.AddOrReplace(key, value); + + public void SetInt64(string key, long value) => _storage.AddOrReplace(key, value); + + public void SetUInt256(string key, UInt256 value) => _storage.AddOrReplace(key, value); + + public void SetString(string key, string value) => _storage.AddOrReplace(key, value); + + public void SetStruct(string key, T value) where T : struct => _storage.AddOrReplace(key, value); + + public void SetUInt32(string key, uint value) => _storage.AddOrReplace(key, value); + + public void SetUInt64(string key, ulong value) => _storage.AddOrReplace(key, value); + + public void SetUInt128(string key, UInt128 value) => _storage.AddOrReplace(key, value); + } +} diff --git a/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/MintableTokenInvoice.Tests.csproj b/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/MintableTokenInvoice.Tests.csproj new file mode 100644 index 00000000..020ee4cf --- /dev/null +++ b/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/MintableTokenInvoice.Tests.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/MintableTokenInvoiceTests.cs b/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/MintableTokenInvoiceTests.cs new file mode 100644 index 00000000..f93ffccb --- /dev/null +++ b/Testnet/MintableTokenInvoice/MintableTokenInvoice.Tests/MintableTokenInvoiceTests.cs @@ -0,0 +1,223 @@ +namespace MintableTokenInvoiceTests; + +using FluentAssertions; +using Moq; +using NBitcoin.DataEncoders; +using Newtonsoft.Json; +using Stratis.SmartContracts; +using Stratis.SmartContracts.CLR; +using Xunit; + +// Claim as defined by Identity Server. +public class Claim +{ + public Claim() + { + } + + public string Key { get; set; } + + public string Description { get; set; } + + public bool IsRevoked { get; set; } +} + +public struct Invoice +{ + public string Symbol; + public UInt256 Amount; + public Address To; + public string Outcome; + public bool IsAuthorized; +} + +/// +/// These tests validate the functionality that differs between the original standard token and the extended version. +/// +public class MintableTokenInvoiceTests : BaseContractTest +{ + [Fact] + public void Constructor_Assigns_Owner() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + + // Verify that PersistentState was called with the contract owner + mintableTokenInvoice.Owner.Should().Be(this.Owner); + } + + [Fact] + public void TransferOwnership_Succeeds_For_Owner() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + mintableTokenInvoice.SetNewOwner(this.AddressOne); + this.SetupMessage(this.Contract, this.AddressOne); + mintableTokenInvoice.ClaimOwnership(); + mintableTokenInvoice.Owner.Should().Be(this.AddressOne); + } + + [Fact] + public void TransferOwnership_Fails_For_NonOwner() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + this.SetupMessage(this.Contract, this.AddressOne); + Assert.ThrowsAny(() => mintableTokenInvoice.SetNewOwner(this.AddressTwo)); + } + + [Fact] + public void CanDeserializeClaim() + { + // First serialize the claim data as the IdentityServer would do it when calling "AddClaim". + var claim = new Claim() { Key = "Identity Approved", Description = "Identity Approved", IsRevoked = false }; + + var bytes = new ASCIIEncoder().DecodeData(JsonConvert.SerializeObject(claim)); + + var json = this.Serializer.ToString(bytes); + + Assert.Contains("Identity Approved", json); + } + + [Fact] + public void CanCreateInvoice() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + + UInt128 uniqueNumber = 1; + + this.SetupMessage(this.Contract, this.AddressOne); + + var claim = new Claim() { Description = "Identity Approved", IsRevoked = false, Key = "Identity Approved" }; + var claimBytes = new ASCIIEncoder().DecodeData(JsonConvert.SerializeObject(claim)); + + this.MockInternalExecutor + .Setup(x => x.Call(It.IsAny(), It.IsAny
(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((ISmartContractState state, Address address, ulong amount, string methodName, object[] args, ulong gasLimit) => + { + return TransferResult.Transferred(claimBytes); + }); + + var transactionReference = mintableTokenInvoice.CreateInvoice("GBPT", 100, uniqueNumber); + var invoiceReference = mintableTokenInvoice.GetInvoiceReference(transactionReference); + + Assert.Equal("INV-1760-4750-2039", invoiceReference.ToString()); + + // 42 is checksum for INV numbers. + Assert.Equal(42UL, ulong.Parse(invoiceReference.Replace("-", string.Empty)[3..]) % 97); + + Assert.Equal("REF-5377-4902-2339", transactionReference.ToString()); + + // 1 is checksum for REF numbers. + Assert.Equal(1UL, ulong.Parse(transactionReference.Replace("-", string.Empty)[3..]) % 97); + + var invoiceBytes = mintableTokenInvoice.RetrieveInvoice(invoiceReference, true); + var invoice = this.Serializer.ToStruct(invoiceBytes); + + Assert.Equal(100, invoice.Amount); + Assert.Equal("GBPT", invoice.Symbol); + Assert.Equal(this.AddressOne, invoice.To); + Assert.True(invoice.IsAuthorized); + Assert.Null(invoice.Outcome); + } + + + [Fact] + public void CantCreateInvoiceIfNotKYCed() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + + UInt128 uniqueNumber = 1; + + this.SetupMessage(this.Contract, this.AddressOne); + + this.MockInternalExecutor + .Setup(x => x.Call(It.IsAny(), It.IsAny
(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((ISmartContractState state, Address address, ulong amount, string methodName, object[] args, ulong gasLimit) => + { + return null; + }); + + var ex = Assert.Throws(() => mintableTokenInvoice.CreateInvoice("GBPT", 100, uniqueNumber)); + Assert.Contains("verification", ex.Message); + } + + [Fact] + public void CantCreateInvoiceIfNotAuthorized() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + + UInt128 uniqueNumber = 1; + + this.SetupMessage(this.Contract, this.AddressOne); + + var claim = new Claim() { Description = "Identity Approved", IsRevoked = false, Key = "Identity Approved" }; + var claimBytes = new ASCIIEncoder().DecodeData(JsonConvert.SerializeObject(claim)); + + this.MockInternalExecutor + .Setup(x => x.Call(It.IsAny(), It.IsAny
(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((ISmartContractState state, Address address, ulong amount, string methodName, object[] args, ulong gasLimit) => + { + return TransferResult.Transferred(claimBytes); + }); + + var ex = Assert.Throws(() => mintableTokenInvoice.CreateInvoice("GBPT", 2000, uniqueNumber)); + Assert.Contains("authorization", ex.Message); + } + + [Fact] + public void CantCreateInvoiceIfDidNotExist() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + + UInt128 uniqueNumber = 1; + + var claim = new Claim() { Description = "Identity Approved", IsRevoked = false, Key = "Identity Approved" }; + var claimBytes = new ASCIIEncoder().DecodeData(JsonConvert.SerializeObject(claim)); + + this.MockInternalExecutor + .Setup(x => x.Call(It.IsAny(), It.IsAny
(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((ISmartContractState state, Address address, ulong amount, string methodName, object[] args, ulong gasLimit) => + { + return TransferResult.Transferred(claimBytes); + }); + + + // The minters will set this status for any payment reference that could not be processed. + // We don't want to process these payments at a later stage as they may get refunded. + mintableTokenInvoice.SetOutcome("REF-5377-4902-2339", "Payment could not be processed"); + + this.SetupMessage(this.Contract, this.AddressOne); + + // Check that we don't "create" an invoice for a payment reference associated with an existing outcome. + var ex = Assert.Throws(() => mintableTokenInvoice.CreateInvoice("GBPT", 200, uniqueNumber)); + Assert.Contains("processed", ex.Message); + } + + [Fact] + public void CanSetIdentityContract() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + + mintableTokenInvoice.SetIdentityContract(this.Contract); + + Assert.Equal(this.Contract, mintableTokenInvoice.IdentityContract); + } + + [Fact] + public void CanSetKYCProvider() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + + mintableTokenInvoice.SetKYCProvider(2); + + Assert.Equal((uint)2, mintableTokenInvoice.KYCProvider); + } + + [Fact] + public void CanSetAuthorizationLimit() + { + var mintableTokenInvoice = this.CreateNewMintableTokenContract(); + + mintableTokenInvoice.SetAuthorizationLimit(300); + + Assert.Equal((UInt256)300, mintableTokenInvoice.AuthorizationLimit); + } +} \ No newline at end of file diff --git a/Testnet/MintableTokenInvoice/MintableTokenInvoice.sln b/Testnet/MintableTokenInvoice/MintableTokenInvoice.sln new file mode 100644 index 00000000..35c1d404 --- /dev/null +++ b/Testnet/MintableTokenInvoice/MintableTokenInvoice.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32901.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MintableTokenInvoice", "MintableTokenInvoice\MintableTokenInvoice.csproj", "{323557EA-5B42-4E72-8E83-948084595898}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MintableTokenInvoice.Tests", "MintableTokenInvoice.Tests\MintableTokenInvoice.Tests.csproj", "{9F6CE3D7-0AD9-425E-8EDE-998F4CAF3716}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {323557EA-5B42-4E72-8E83-948084595898}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {323557EA-5B42-4E72-8E83-948084595898}.Debug|Any CPU.Build.0 = Debug|Any CPU + {323557EA-5B42-4E72-8E83-948084595898}.Release|Any CPU.ActiveCfg = Release|Any CPU + {323557EA-5B42-4E72-8E83-948084595898}.Release|Any CPU.Build.0 = Release|Any CPU + {9F6CE3D7-0AD9-425E-8EDE-998F4CAF3716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F6CE3D7-0AD9-425E-8EDE-998F4CAF3716}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F6CE3D7-0AD9-425E-8EDE-998F4CAF3716}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F6CE3D7-0AD9-425E-8EDE-998F4CAF3716}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1E2C5ABC-4F7B-4E76-A1B5-662A56FBC1D2} + EndGlobalSection +EndGlobal diff --git a/Testnet/MintableTokenInvoice/MintableTokenInvoice/IPullOwnership.cs b/Testnet/MintableTokenInvoice/MintableTokenInvoice/IPullOwnership.cs new file mode 100644 index 00000000..e3a2138d --- /dev/null +++ b/Testnet/MintableTokenInvoice/MintableTokenInvoice/IPullOwnership.cs @@ -0,0 +1,21 @@ +using Stratis.SmartContracts; + +/// +/// Provides the notion of ownership to the token contract. +/// The owner is able to perform certain privileged operations not available to generic users. +/// +public interface IPullOwnership +{ + Address Owner { get; } + + /// + /// Assign ownership tentatively. + /// + /// The address of the new owner. + void SetNewOwner(Address address); + + /// + /// Called by the new owner of the contract to claim ownership. + /// + void ClaimOwnership(); +} \ No newline at end of file diff --git a/Testnet/MintableTokenInvoice/MintableTokenInvoice/MintableTokenInvoice.cs b/Testnet/MintableTokenInvoice/MintableTokenInvoice/MintableTokenInvoice.cs new file mode 100644 index 00000000..6954b422 --- /dev/null +++ b/Testnet/MintableTokenInvoice/MintableTokenInvoice/MintableTokenInvoice.cs @@ -0,0 +1,302 @@ +using Stratis.SmartContracts; +using System; + +/// +/// Implementation of a mintable token invoice contract for the Stratis Platform. +/// +[Deploy] +public class MintableTokenInvoice : SmartContract, IPullOwnership +{ + /// + /// Constructor used to create a new instance of the token. Assigns the total token supply to the creator of the contract. + /// + /// The execution state for the contract. + /// Any amounts greater or equal to this will require authorization. + /// The address of the identity contract. + public MintableTokenInvoice(ISmartContractState smartContractState, UInt256 authorizationLimit, Address identityContract) : base(smartContractState) + { + this.Owner = Message.Sender; + this.NewOwner = Address.Zero; + this.AuthorizationLimit = authorizationLimit; + this.IdentityContract = identityContract; + this.KYCProvider = 3 /* ClaimTopic.Shufti */; + } + + /// + public Address Owner + { + get => State.GetAddress(nameof(this.Owner)); + private set => State.SetAddress(nameof(this.Owner), value); + } + + public Address NewOwner + { + get => State.GetAddress(nameof(this.NewOwner)); + private set => State.SetAddress(nameof(this.NewOwner), value); + } + + public UInt256 AuthorizationLimit + { + get => State.GetUInt256(nameof(this.AuthorizationLimit)); + private set => State.SetUInt256(nameof(this.AuthorizationLimit), value); + } + + public Address IdentityContract + { + get => State.GetAddress(nameof(this.IdentityContract)); + private set => State.SetAddress(nameof(this.IdentityContract), value); + } + + public uint KYCProvider + { + get => State.GetUInt32(nameof(KYCProvider)); + private set => State.SetUInt32(nameof(this.KYCProvider), value); + } + + private void SetInvoice(string invoiceReference, Invoice invoice) + { + State.SetStruct($"Invoice:{invoiceReference}", invoice); + } + + private Invoice GetInvoice(string invoiceReference) + { + return State.GetStruct($"Invoice:{invoiceReference}"); + } + + private struct TransactionReferenceTemplate + { + public UInt128 uniqueNumber; + public Address address; + } + + private string GetTransactionReference(UInt128 uniqueNumber) + { + var template = new TransactionReferenceTemplate() { uniqueNumber = uniqueNumber, address = Message.Sender }; + + var res = Serializer.Serialize(template); + + var temp = Keccak256(res); + var transactionReference = $"{((Serializer.ToUInt256(temp) % 10000000000) * 97 + 1)}".PadLeft(12, '0'); + + return $"REF-{transactionReference.Substring(0, 4)}-{transactionReference.Substring(4, 4)}-{transactionReference.Substring(8, 4)}"; + } + + public string GetInvoiceReference(string transactionReference) + { + // Hash the transaction reference to get the invoice reference. + // This avoids the transaction reference being exposed in the SC state. + var temp = Keccak256(Serializer.Serialize(transactionReference)); + var invoiceReference = $"{((Serializer.ToUInt256(temp) % 10000000000) * 97 + 42)}".PadLeft(12, '0'); + + return $"INV-{invoiceReference.Substring(0, 4)}-{invoiceReference.Substring(4, 4)}-{invoiceReference.Substring(8, 4)}"; + } + + private string ValidateKYC(Address sender, string invoiceReference) + { + // KYC check. Call Identity contract. + ITransferResult result = this.Call(IdentityContract, 0, "GetClaim", new object[] { sender, KYCProvider }); + if (!(result?.Success ?? false)) + { + string reason = "Could not determine KYC status"; + Log(new InvoiceResult() { InvoiceReference = invoiceReference, Success = false, Reason = reason }); + return reason; + } + + // The return value is a json string encoding of a Model.Claim object, represented as a byte array using ascii encoding. + // The "Key" and "Description" fields of the json-encoded "Claim" object are expected to contain "Identity Approved". + if (result.ReturnValue == null || !Serializer.ToString((byte[])result.ReturnValue).Contains("Identity Approved")) + { + string reason = "Your KYC status is not valid"; + Log(new InvoiceResult() { InvoiceReference = invoiceReference, Success = false, Reason = reason }); + return reason; + } + + return string.Empty; + } + + /// + public string CreateInvoice(string symbol, UInt256 amount, UInt128 uniqueNumber) + { + string transactionReference = GetTransactionReference(uniqueNumber); + + var invoiceReference = GetInvoiceReference(transactionReference); + + // Ensure that this method can be called multiple times until all issues are resolved. + var invoice = GetInvoice(invoiceReference); + if (invoice.To != Address.Zero) + // If called with the same unique number then the details should not change. + Assert(invoice.To != Message.Sender || invoice.Symbol != symbol && invoice.Amount != amount, "Transaction reference already exists"); + else + // Allow the outcome of an invoice to be set when only references have been provided. + invoice = new Invoice() { Symbol = symbol, Amount = amount, To = Message.Sender, Outcome = invoice.Outcome, IsAuthorized = amount < AuthorizationLimit }; + + // If the invoice already has an outcome then just return it. + Assert(string.IsNullOrEmpty(invoice.Outcome), invoice.Outcome); + + string result = ValidateKYC(Message.Sender, invoiceReference); + Assert(string.IsNullOrEmpty(result), "Obtain KYC verification for this address and then resubmit this request."); + + SetInvoice(invoiceReference, invoice); + + Assert(invoice.IsAuthorized, $"Obtain authorization for this invoice ({invoiceReference}) then resubmit this request."); + + Log(new InvoiceResult() { InvoiceReference = invoiceReference, Success = true }); + + // Only provide the transaction reference if all checks pass. + return transactionReference; + } + + /// + public byte[] RetrieveInvoice(string invoiceReference, bool recheckKYC) + { + var invoice = GetInvoice(invoiceReference); + + // Only recheck KYC on invoices that have not yet been processed. + if (recheckKYC && invoice.To != Address.Zero && string.IsNullOrEmpty(invoice.Outcome)) + { + // Do another last minute KYC check just in case the KYC was revoked since the invoice was created. + if (recheckKYC) + ValidateKYC(invoice.To, invoiceReference); + } + + return Serializer.Serialize(invoice); + } + + private void EnsureOwnerOnly() + { + Assert(Owner == Message.Sender, "Only the owner can call this method."); + } + + public bool AuthorizeInvoice(string invoiceReference) + { + EnsureOwnerOnly(); + + var invoice = GetInvoice(invoiceReference); + + Assert(invoice.To != Address.Zero, "The invoice does not exist."); + Assert(!string.IsNullOrEmpty(invoice.Outcome), "The transaction has already been processed."); + + invoice.IsAuthorized = true; + SetInvoice(invoiceReference, invoice); + + Log(new ChangeInvoiceAuthorization() { InvoiceReference = invoiceReference, NewAuthorized = true, OldAuthorized = invoice.IsAuthorized }); + + return true; + } + + /// + public void SetAuthorizationLimit(UInt256 newLimit) + { + EnsureOwnerOnly(); + + Log(new ChangeAuthorizationLimit() { OldLimit = AuthorizationLimit, NewLimit = newLimit }); + + AuthorizationLimit = newLimit; + } + + public void SetOutcome(string transactionReference, string outcome) + { + EnsureOwnerOnly(); + + var invoiceReference = GetInvoiceReference(transactionReference); + + var invoice = GetInvoice(invoiceReference); + invoice.Outcome = outcome; + SetInvoice(invoiceReference, invoice); + } + + /// + public void SetIdentityContract(Address identityContract) + { + EnsureOwnerOnly(); + + Log(new ChangeIdentityContract() { OldContract = IdentityContract, NewContract = identityContract }); + + IdentityContract = identityContract; + } + + /// + public void SetKYCProvider(uint kycProvider) + { + EnsureOwnerOnly(); + + Log(new ChangeKYCProvider() { OldProvider = KYCProvider, NewProvider = kycProvider }); + + KYCProvider = kycProvider; + } + + /// + public void SetNewOwner(Address address) + { + EnsureOwnerOnly(); + + NewOwner = address; + } + + /// + public void ClaimOwnership() + { + Assert(Message.Sender == NewOwner, "Only the new owner can call this method"); + + var previousOwner = Owner; + + Owner = NewOwner; + + NewOwner = Address.Zero; + + Log(new OwnershipTransferred() { NewOwner = Message.Sender, PreviousOwner = previousOwner }); + } + + /// + /// Provides a record that ownership was transferred from one account to another. + /// + public struct OwnershipTransferred + { + [Index] public Address PreviousOwner; + [Index] public Address NewOwner; + } + + /// + /// Holds the details for the minting operation. + /// + public struct Invoice + { + public string Symbol; + public UInt256 Amount; + public Address To; + public string Outcome; + public bool IsAuthorized; + } + + public struct InvoiceResult + { + [Index] public string InvoiceReference; + public bool Success; + public string Reason; + } + + public struct ChangeAuthorizationLimit + { + public UInt256 OldLimit; + public UInt256 NewLimit; + } + + public struct ChangeKYCProvider + { + public uint OldProvider; + public uint NewProvider; + } + + public struct ChangeIdentityContract + { + public Address OldContract; + public Address NewContract; + } + + public struct ChangeInvoiceAuthorization + { + [Index] public string InvoiceReference; + public bool OldAuthorized; + public bool NewAuthorized; + } +} \ No newline at end of file diff --git a/Testnet/MintableTokenInvoice/MintableTokenInvoice/MintableTokenInvoice.csproj b/Testnet/MintableTokenInvoice/MintableTokenInvoice/MintableTokenInvoice.csproj new file mode 100644 index 00000000..2d3aa2e9 --- /dev/null +++ b/Testnet/MintableTokenInvoice/MintableTokenInvoice/MintableTokenInvoice.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + + + + + + + + diff --git a/Testnet/MintableTokenInvoice/README.MD b/Testnet/MintableTokenInvoice/README.MD new file mode 100644 index 00000000..3281e915 --- /dev/null +++ b/Testnet/MintableTokenInvoice/README.MD @@ -0,0 +1,19 @@ +# MintableTokenInvoice + +**Compiler Version** + +``` +v2.0.0 +``` + +**Contract Hash** +``` +603c39b401c32081e69f8847bc44783fdf256970c5319b213cbc390659a1d44f +``` + +**Contract Byte Code** +``` +4D5A90000300000004000000FFFF0000B800000000000000400000000000000000000000000000000000000000000000000000000000000000000000800000000E1FBA0E00B409CD21B8014CCD21546869732070726F6772616D2063616E6E6F742062652072756E20696E20444F53206D6F64652E0D0D0A2400000000000000504500004C0102004BAF41D40000000000000000E00022200B013000001C000000020000000000009E3B00000020000000400000000000100020000000020000040000000000000004000000000000000060000000020000000000000300408500001000001000000000100000100000000000001000000000000000000000004C3B00004F000000000000000000000000000000000000000000000000000000004000000C000000303B00001C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000080000000000000000000000082000004800000000000000000000002E74657874000000A41B000000200000001C000000020000000000000000000000000000200000602E72656C6F6300000C0000000040000000020000001E0000000000000000000000000000400000420000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000803B00000000000048000000020005005C270000D41300000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000E60203280600000A0202280700000A6F0800000A2806000006027E0900000A28080000060204280A0000060205280C0000060219280E0000062A4602280A00000A72010000706F0B00000A2A4A02280A00000A7201000070036F0C00000A2A4602280A00000A720D0000706F0B00000A2A4A02280A00000A720D000070036F0C00000A2A4602280A00000A721F0000706F0D00000A2A4A02280A00000A721F000070036F0E00000A2A4602280A00000A72450000706F0B00000A2A4A02280A00000A7245000070036F0C00000A2A4602280A00000A72670000706F0F00000A2A4A02280A00000A7267000070036F1000000A2A6202280A00000A727F00007003281100000A046F0100002B2A5E02280A00000A727F00007003281100000A6F0200002B2A13300600AC000000010000111204FE15040000021204037D01000004120402280700000A6F0800000A7D0200000411040A02281400000A066F0300002B0B0207281600000A0C729700007002281400000A086F1700000A2100E40B5402000000281800000A281900000A1F61281A00000A281B00000A17281A00000A281C00000A8C09000001281100000A1F0C1F306F1D00000A0D729F00007009161A6F1E00000A091A1A6F1E00000A091E1A6F1E00000A281F00000A2A1330060086000000020000110202281400000A036F2000000A281600000A0A729700007002281400000A066F1700000A2100E40B5402000000281800000A281900000A1F61281A00000A281B00000A1F2A281A00000A281C00000A8C09000001281100000A1F0C1F306F1D00000A0B72BF00007007161A6F1E00000A071A1A6F1E00000A071E1A6F1E00000A281F00000A2A000013300800CC000000030000110202280B000006166A72DF000070188D110000012516038C05000001A2251702280D0000068C13000001A2166A282100000A0A062C08066F2200000A2D2F72F10000700B021202FE15070000021202047D0A0000041202167D0B0000041202077D0C00000408280400002B072A066F2400000A2C2202281400000A066F2400000A740100001B6F2500000A722F0100706F2600000A2D2F72530100700D021202FE15070000021202047D0A0000041202167D0B0000041202097D0C00000408280400002B092A7E2700000A2A133004003801000004000011020528110000060A020628120000060B020728100000060C087B070000047E0900000A282800000A2C4502087B0700000402280700000A6F0800000A282800000A2D1F087B0500000403282900000A2C0E087B0600000404282A00000A2B04162B0117728D010070282B00000A2B4D1204FE15060000021204037D050000041204047D06000004120402280700000A6F0800000A7D070000041204087B080000047D08000004120404022809000006282C00000A7D0900000411040C02087B08000004282D00000A087B08000004282B00000A0202280700000A6F0800000A0728130000060D0209282D00000A72D7010070282B00000A020708280F00000602087B09000004726A02007007281100000A282B00000A021205FE15070000021205077D0A0000041205177D0B0000041105280400002B062A133003004800000005000011020328100000060A042C30067B070000047E0900000A282800000A2C1E067B08000004282D00000A2C11042C0E02067B070000040328130000062602281400000A066F0500002B2A8A0202280500000602280700000A6F0800000A282E00000A72FB020070282B00000A2A00133003008000000006000011022816000006020328100000060A02067B070000047E0900000A282800000A7245030070282B00000A02067B08000004282D00000A16FE01727D030070282B00000A1200177D09000004020306280F000006021201FE150B0000021201037D130000041201177D150000041201067B090000047D1400000407280600002B172A133003003200000007000011022816000006021200FE150800000212000228090000067D0D0000041200037D0E00000406280700002B0203280A0000062A0000133003002700000008000011022816000006020328120000060A020628100000060B1201047D08000004020607280F0000062A00133003003200000009000011022816000006021200FE150A000002120002280B0000067D110000041200037D1200000406280800002B0203280C0000062A000013300300320000000A000011022816000006021200FE1509000002120002280D0000067D0F0000041200037D1000000406280900002B0203280E0000062A3A022816000006020328080000062A00000013300300690000000B0000110202280700000A6F0800000A022807000006282E00000A72D5030070282B00000A0228050000060A020228070000062806000006027E0900000A2808000006021201FE1505000002120102280700000A6F0800000A7D040000041201067D0300000407280A00002B2A00000042534A4201000100000000000C00000076342E302E33303331390000000005006C00000088060000237E0000F40600008006000023537472696E677300000000740D000028040000235553009C110000100000002347554944000000AC1100002802000023426C6F6200000000000000020000015717A209090A000000FA01330016000001000000130000000B000000150000001D0000001C000000010000002E000000080000000B00000002000000060000000B000000010000000100000002000000080000000A0000000000AE020100000000000600150235040600540235040600010222040F00550400000A00870497040A00440297040A00CC0497040A00C90197040A00370097040A003F0097040A001406970406009801CE020A00350297040A00700197040A00F001970406009402CE0206004C05CE020A000104970406000E00CE020000000047000000000001000100A10000001703000000000100010001001000EB0000001D00010004000B011000A2010000310001001E000A0110007E000000310003001E000A0110000E010000310005001E000A0110000606000031000A001E000A011000B205000031000D001E000A0110006203000031000F001E000A01100010050000310011001E000A011000E1020000310013001E00060035037A0106008F0420000600CD0320000600F80320000600C0020001060024067E0106000F0320000600880100010600A100820106002101000106007104820106000803000106007B057E010600F4057E0106008F03850106009B0385010600C00420000600DA0420000600210100010600930082010600AE008201000000000000C60DB9031B000100000000000000C605F50388010100000000000000C60526030600020050200000000086181C048E0102008A2000000000E609B9031B0005009C20000000008108C30388010500AF20000000008608DB031B000600C120000000008108E80388010600D420000000008608840598010700E6200000000081089B059D010700F920000000008608E6041B0008000B21000000008108FB04880108001E210000000086084203A301090030210000000081085203A701090043210000000081000B01AC010A005C210000000081000001B3010C0074210000000081004301B9010D002C220000000086001E01BF010E00C0220000000081005000C4010F009823000000008600BC00CB011100DC24000000008600CA00D401140030250000000081003906060016005425000000008600DA00FB001600E025000000008600CB059D01170020260000000086008501DB0118005426000000008600270588011A0094260000000086007403A7011B00D22600000000E601F50388011C00E42600000000E601260306001D00000001008F0400000100DD0100000200E105000003003B05000001008202000001008202000001008202000001008202000001008202000001003201000002001601000001003201000001003503000001005B0100000100B20300000200320100000100C702000002002B06000003003503000001003201000002005C0000000100320100000100FD05000001005B01000002009001000001003B05000001008303000001008F040300080009001C04010011001C04060019001C040A0031001C04060069001C04060039001C0410003900790116007100A7031B002900120320003900BF012400790079042900790084042F00790029003600790034003C0079000100430079000B0048008100B9044E0079005D05540079005305610039000D04740091008802790039001500860091001F008D0049006F0594004900AE049A0049006F05A300490049069A004900FC029A0081006705A90081009B02AF008100B904B50091008802C3003900A902D20059006D04DE003900A502E20059007202EE0091009202F50081006404FB00810077060001290061060F01810061061701490061061D013900320625014900D5021D0181006F062B01290055060F012E000B00EF012E001300F8012E001B00170261002B00200263002300200281002B00200241012B00200261022B0020026800BD00C90003013001350141014B0151015B01650102000100030002000000FB03E1010000FB03E1010000F803E1010000CE05E60100002A05E10100007703EB0102000100030002000500050001000600050002000700070001000800070002000900090001000A00090002000B000B0001000C000B0002000D000D0001000E000D00F200048000000000000000000000000000000000CC040000040000000000000000000000710167000000000002000000000000000000000000009704000000000400030005000300060003000700030008000300090003000A0003000B00030025005C0027005C002B0081004700E9002B005C0047003C0147004601470056014700600147006C0100000047657455496E7433320053657455496E743332004B656363616B32353600546F55496E743235360047657455496E743235360053657455496E743235360055496E74313238003C4D6F64756C653E0056616C69646174654B5943007265636865636B4B59430053797374656D2E507269766174652E436F72654C6962004F776E6572736869705472616E73666572726564004F6C64417574686F72697A6564004973417574686F72697A6564004E6577417574686F72697A656400437265617465496E766F696365005265747269657665496E766F69636500417574686F72697A65496E766F696365004D696E7461626C65546F6B656E496E766F69636500476574496E766F69636500536574496E766F69636500696E766F69636500476574496E766F6963655265666572656E636500696E766F6963655265666572656E6365004765745472616E73616374696F6E5265666572656E6365007472616E73616374696F6E5265666572656E636500494D657373616765006765745F4D657373616765005365744F7574636F6D65006F7574636F6D650056616C756554797065005472616E73616374696F6E5265666572656E636554656D706C617465006765745F53746174650049536D617274436F6E7472616374537461746500736D617274436F6E74726163745374617465004950657273697374656E7453746174650044656275676761626C6541747472696275746500436F6D70696C6174696F6E52656C61786174696F6E7341747472696275746500496E646578417474726962757465004465706C6F794174747269627574650052756E74696D65436F6D7061746962696C697479417474726962757465006765745F52657475726E56616C75650076616C75650053657269616C697A6500546F537472696E6700537562737472696E67004C6F670043616C6C00536D617274436F6E74726163742E646C6C0053796D626F6C0073796D626F6C0053797374656D006F705F4C6573735468616E004368616E6765496E766F696365417574686F72697A6174696F6E006F705F4164646974696F6E00526561736F6E00546F005A65726F004950756C6C4F776E65727368697000436C61696D4F776E65727368697000756E697175654E756D626572006765745F4B594350726F7669646572007365745F4B594350726F7669646572004368616E67654B594350726F7669646572005365744B594350726F7669646572006B796350726F7669646572004F6C6450726F7669646572004E657750726F7669646572006765745F53656E6465720073656E646572006765745F4F776E6572007365745F4F776E65720050726576696F75734F776E6572006765745F4E65774F776E6572007365745F4E65774F776E6572005365744E65774F776E6572004953657269616C697A6572006765745F53657269616C697A6572002E63746F720053797374656D2E446961676E6F73746963730053797374656D2E52756E74696D652E436F6D70696C6572536572766963657300446562756767696E674D6F64657300436F6E7461696E73006765745F5375636365737300476574416464726573730053657441646472657373006164647265737300537472617469732E536D617274436F6E747261637473006F705F4D6F64756C757300466F726D6174004F6C64436F6E747261637400536D617274436F6E7472616374004E6577436F6E7472616374006765745F4964656E74697479436F6E7472616374007365745F4964656E74697479436F6E7472616374004368616E67654964656E74697479436F6E7472616374005365744964656E74697479436F6E7472616374006964656E74697479436F6E7472616374004F626A6563740047657453747275637400536574537472756374005061644C656674006F705F496D706C69636974004F6C644C696D6974006765745F417574686F72697A6174696F6E4C696D6974007365745F417574686F72697A6174696F6E4C696D6974004368616E6765417574686F72697A6174696F6E4C696D697400536574417574686F72697A6174696F6E4C696D697400617574686F72697A6174696F6E4C696D6974004E65774C696D6974006E65774C696D697400496E766F696365526573756C7400495472616E73666572526573756C7400416D6F756E7400616D6F756E740041737365727400456E737572654F776E65724F6E6C79006F705F4D756C7469706C79006F705F457175616C697479006F705F496E657175616C6974790049734E756C6C4F72456D70747900000000000B4F0077006E006500720000114E00650077004F0077006E0065007200002541007500740068006F00720069007A006100740069006F006E004C0069006D006900740000214900640065006E00740069007400790043006F006E007400720061006300740000174B0059004300500072006F0076006900640065007200001749006E0076006F006900630065003A007B0030007D0000077B0030007D00001F5200450046002D007B0030007D002D007B0031007D002D007B0032007D00011F49004E0056002D007B0030007D002D007B0031007D002D007B0032007D00011147006500740043006C00610069006D00003D43006F0075006C00640020006E006F0074002000640065007400650072006D0069006E00650020004B0059004300200073007400610074007500730000234900640065006E007400690074007900200041007000700072006F00760065006400003959006F007500720020004B0059004300200073007400610074007500730020006900730020006E006F0074002000760061006C006900640000495400720061006E00730061006300740069006F006E0020007200650066006500720065006E0063006500200061006C007200650061006400790020006500780069007300740073000080914F0062007400610069006E0020004B0059004300200076006500720069006600690063006100740069006F006E00200066006F0072002000740068006900730020006100640064007200650073007300200061006E00640020007400680065006E002000720065007300750062006D006900740020007400680069007300200072006500710075006500730074002E0000808F4F0062007400610069006E00200061007500740068006F00720069007A006100740069006F006E00200066006F00720020007400680069007300200069006E0076006F00690063006500200028007B0030007D00290020007400680065006E002000720065007300750062006D006900740020007400680069007300200072006500710075006500730074002E0000494F006E006C007900200074006800650020006F0077006E00650072002000630061006E002000630061006C006C002000740068006900730020006D006500740068006F0064002E000037540068006500200069006E0076006F00690063006500200064006F006500730020006E006F0074002000650078006900730074002E00005754006800650020007400720061006E00730061006300740069006F006E002000680061007300200061006C007200650061006400790020006200650065006E002000700072006F006300650073007300650064002E00004F4F006E006C007900200074006800650020006E006500770020006F0077006E00650072002000630061006E002000630061006C006C002000740068006900730020006D006500740068006F00640000000000BAAA39DF6CCB054F9649BA889FDDBF68000420010108032000010520010111110520010112210420001239042000111503061115042000123D05200111150E062002010E111505200111250E062002010E1125042001090E052002010E090500020E0E1C07300102010E1E00040A011118063001011E000E0B070511101D051D050E11100420001249073001011D051E00040A0111100620011D051D0506200111251D0505000111250A0800021125112511250500011125080520020E08030520020E08080700040E0E1C1C1C0507021D050E0520011D050E080704122D0E111C0E0B2005122D11150B0E1D1C0B0320000206300101011E00040A01111C0320001C021D050520010E1D05042001020E02060E0B07060E0E11180E1118111C0700020211151115050002020E0E070002021125112505200201020E040001020E04070111180607021118112C040A01112C0407011120040A0111200507020E11180407011128040A0111280407011124040A01112406070211151114040A011114087CEC85D7BEA7798E0306112903061125020602020609052001011115092003011221112511150420001125052001011125032000090420010109062002010E111805200111180E0520010E11290420010E0E0620020E11150E0820030E0E112511290620021D050E02052002010E0E04280011150428001125032800090801000800000000001E01000100540216577261704E6F6E457863657074696F6E5468726F777301080100020000000000040100000000000000000000000000000000000010000000000000000000000000000000743B000000000000000000008E3B0000002000000000000000000000000000000000000000000000803B0000000000000000000000005F436F72446C6C4D61696E006D73636F7265652E646C6C0000000000FF25002000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000C000000A03B00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +``` + +