diff --git a/.github/format.sh b/.github/format.sh new file mode 100755 index 000000000..840c1e997 --- /dev/null +++ b/.github/format.sh @@ -0,0 +1,4 @@ +# Extensions necessary to tell fourmolu about +EXTENSIONS="-o -XTypeApplications -o -XTemplateHaskell -o -XImportQualifiedPost -o -XPatternSynonyms -o -fplugin=RecordDotPreprocessor" +SOURCES=$(git ls-tree -r HEAD --full-tree --name-only | grep -E '.*\.hs') +fourmolu --mode check --check-idempotence $EXTENSIONS $SOURCES diff --git a/.github/local-formatting.sh b/.github/local-formatting.sh new file mode 100755 index 000000000..2d9e22851 --- /dev/null +++ b/.github/local-formatting.sh @@ -0,0 +1,4 @@ +# Extensions necessary to tell fourmolu about +EXTENSIONS="-o -XTypeApplications -o -XTemplateHaskell -o -XImportQualifiedPost -o -XPatternSynonyms -o -fplugin=RecordDotPreprocessor" +SOURCES=$(git ls-tree -r HEAD --full-tree --name-only | grep -E '.*\.hs') +fourmolu --mode inplace --check-idempotence $EXTENSIONS $SOURCES diff --git a/STANDARDS.md b/STANDARDS.md new file mode 100644 index 000000000..4f33b859f --- /dev/null +++ b/STANDARDS.md @@ -0,0 +1,1027 @@ +# Introduction + +This document describes a set of standards for all code under the Plutus Use Cases +project. It also explains our reasoning for these choices, and acts as a living +document of our practices for current and future contributors to the project. We +intend for this document to evolve as our needs change, as well as act as a +single point of truth for standards. + +# Motivation + +The desired outcomes from the prescriptions in this document are as follows. + +## Increase consistency + +Inconsistency is worse than _any_ standard, as it requires us to track a large +amount of case-specific information. Software development is already a difficult +task due to the inherent complexities of the problems we seek to solve, as well +as the inherent complexities foisted upon us by _decades_ of bad historical +choices we have no control over. For newcomers to a project and old hands alike, +increased inconsistency translates to developmental friction, resulting in +wasted time, frustration and ultimately, worse outcomes for the code in +question. + +To avoid putting ourselves into this boat, both currently and in the future, we +must strive to be _automatically consistent_. Similar things should look +similar; different things should look different; as much as possible, we must +pick some rules _and stick to them_; and this has to be clear, explicit and +well-motivated. This will ultimately benefit us, in both the short and the long +term. The standards described here, as well as this document itself, is written +with this foremost in mind. + +## Limit non-local information + +There is a limited amount of space in a developer's skull; we all have bad days, +and we forget things or make decisions that, perhaps, may not be ideal at the +time. Therefore, limiting cognitive load is good for us, as it reduces the +amount of trouble we can inflict due to said skull limitations. One of the worst +contributors to cognitive load (after inconsistency) is _non-local information_ +- the requirement to have some understanding beyond the scope of the current +unit of work. That unit of work can be a data type, a module, or even a whole +project; in all cases, the more non-local information we require ourselves to +hold in our minds, the less space that leaves for actually doing the task at +hand, and the more errors we will introduce as a consequence. + +Thus, we must limit the need for non-local information at all possible levels. +'Magic' of any sort must be avoided; as much locality as possible must be +present everywhere; needless duplication of effort or result must be avoided. +Thus, our work must be broken down into discrete, minimal, logical units, which +can be analyzed, worked on, reviewed and tested in as much isolation as +possible. This also applies to our external dependencies. + +Thus, many of the decisions described here are oriented around limiting the +amount of non-local knowledge required at all levels of the codebase. +Additionally, we aim to avoid doing things 'just because we can' in a way that +would be difficult for other Haskellers to follow, regardless of skill level. + +## Minimize impact of legacy + +Haskell is a language that is older than some of the people currently writing +it; parts of its ecosystem are not exempt from it. With age comes legacy, and +much of it is based on historical decisions which we now know to be problematic +or wrong. We can't avoid our history, but we can minimize its impact on our +current work. + +Thus, we aim to codify good practices in this document _as seen today_. We also +try to avoid obvious 'sharp edges' by proscribing them away in a principled, +consistent and justifiable manner. + +## Automate away drudgery + +As developers, we should use our tools to make ourselves as productive as +possible. There is no reason for us to do a task if a machine could do it for +us, especially when this task is something boring or repetitive. We love Haskell +as a language not least of all for its capability to abstract, to describe, and +to make fun what other languages make dull or impossible; likewise, our work +must do the same. + +Many of the tool-related proscriptions and requirements in this document are +driven by a desire to remove boring, repetitive tasks that don't need a human to +perform. By removing the need for us to think about such things, we can focus on +those things which _do_ need a human; thus, we get more done, quicker. + +# Conventions + +The words MUST, SHOULD, MUST NOT, SHOULD NOT and MAY are defined as per [RFC +2119][rfc-2119]. + +# Tools + +## Compiler warning settings + +The following warnings MUST be enabled for all builds of any project, or any +project component: + +* ``-Wall`` +* ``-Wcompat`` +* ``-Wincomplete-uni-patterns`` +* ``-Wredundant-constraints`` +* ``-Werror`` + +Additionally, ``-Wincomplete-record-updates`` SHOULD be enabled for all builds +of any project. The only exception is when this warning would be spuriously +triggered by ``record-dot-preprocessor``, which occurs for definitions like +this: + +```haskell +data Foo = Bar { + baz :: Int, + quux :: String + } | + Quux +``` + +Additionally, ``-Wredundant-constraints`` SHOULD be enabled for all builds of +any project. Exceptions are allowed when the additional constraints are designed +to ensure safety, rather than due to reliance on any method. + +If a warning from this list is to be disabled, it MUST be disabled in the +narrowest possible scope; ideally, this SHOULD be a single module. + +### Justification + +These options are suggested by [Alexis King][alexis-king-options] - the +justifications for them can be found at the link. These fit well with our +motivations, and thus, should be used everywhere. The ``-Werror`` ensures that +warnings _cannot_ be ignored: this means that problems get fixed sooner. + +The two permissible exceptions stem from limitations in the record-dot plugin +(for ``-Wincomplete-record-updates``) and from the way redundant constraints are +detected; basically, unless a type class method from a constraint is used within +the body of the definition, or is required by anything called in a transitive +manner, the constraint is deemed redundant. Mostly, this is accurate, but some +type-level safety constraints can be deemed redundant as a result of this +approach. In this case, a limited lowering (per module ideally) of those two +warnings is acceptable, as they represent workarounds to technical problems, +rather than issues with the warnings themselves. + +## Linting + +Every source file MUST be free of warnings as produced by [HLint][hlint], with +default settings. + +### Justification + +HLint automates away the detection of many common sources of boilerplate and +inefficiency. It also describes many useful refactors, which in many cases make +the code easier to read and understand. As this is fully automatic, it saves +effort on our part, and ensures consistency across the codebase without us +having to think about it. + +## Code formatting + +Every source file MUST be formatted according to [Fourmolu][fourmolu], with the +following settings (as per its settings file): + +* ``indentation: 2`` +* ``comma-style: leading`` +* ``record-brace-space: true`` +* ``indent-wheres: true`` +* ``diff-friendly-import-export: true`` +* ``respectful: true`` +* ``haddock-style: multi-line`` +* ``newlines-between-decls: 1`` + +Each source code line MUST be at most 100 characters wide, and SHOULD +be at most 80 characters wide. + +### Justification + +Consistency is the most important goal of readable codebases. Having a single +standard, automatically enforced, means that we can be sure that everything will +look similar, and not have to spend time or mind-space ensuring that our code +complies. Additionally, as Ormolu is opinionated, anyone familiar with its +layout will find our code familiar, which eases the learning curve. + +Lines wider than 80 characters become difficult to read, especially when viewed +on a split screen. Sometimes, we can't avoid longer lines (especially with more +descriptive identifiers), but a line length of over 100 characters becomes +difficult to read even without a split screen. We don't _enforce_ a maximum of +80 characters for this exact reason; some judgment is allowed. + +# Code practices + +## Naming + +camelCase MUST be used for all non-type, non-data-constructor names; otherwise, +TitleCase MUST be used. Acronyms used as part of a naming identifier (such as +'JSON', 'API', etc) SHOULD be downcased; thus ``repairJson`` and +``fromHttpService`` are correct. Exceptions are allowed for external libraries +(Aeson's ``parseJSON`` for example). + +### Justification + +camelCase for non-type, non-data-constructor names is a long-standing convention +in Haskell (in fact, HLint checks for it); TitleCase for type names or data +constructors is _mandatory_. Obeying such conventions reduces cognitive load, as +it is common practice among the entire Haskell ecosystem. There is no particular +standard regarding acronym casing: examples of always upcasing exist (Aeson) as +well as examples of downcasing (``http-api-data``). One choice for consistency +(or as much as is possible) should be made however. + +## Modules + +All publically facing modules (namely, those which are not listed in +``other-modules`` in the Cabal file) MUST have explicit export lists. + +All modules MUST use one of the following conventions for imports: + +* ``import Foo (Baz, Bar, quux)`` +* ``import qualified Foo as F`` + +Data types from qualified-imported modules SHOULD be imported unqualified by +themselves: + +```haskell +import Data.Vector (Vector) +import qualified Data.Vector as Vector +``` + +The main exception is if such an import would cause a name clash: + +```haskell +-- no way to import both of these without clashing the Vector type name +import qualified Data.Vector as Vector +import qualified Data.Vector.Storable as VStorable +``` + +The _sole_ exception is a 'hiding import' to replace part of the functionality +of ``Prelude``: + +```haskell +-- replace the String-based readFile with a Text-based one +import Prelude hiding (readFile) +import Data.Text.IO (readFile) +``` + +Data constructors SHOULD be imported individually. For example, given the +following data type declaration: + +```haskell +module Quux where + +data Foo = Bar Int | Baz +``` + +Its corresponding import should be: + +```haskell +import Quux (Foo, Bar, Baz) +``` + +For type class methods, the type class and its methods MUST be imported +as so: + +```haskell +import Data.Aeson (FromJSON (fromJSON)) +``` + +Qualified imports SHOULD use the entire module name (that is, the last component +of its hierarchical name) as the prefix. For example: + +```haskell +import qualified Data.Vector as Vector +``` + +Exceptions are granted when: + +* The import would cause a name clash anyway (such as different ``vector`` + modules); or +* We have to import a data type qualified as well. + +Qualified imports of multiple modules MUST NOT be imported under the same name. +Thus, the following is wrong: + +```haskell +import qualified Foo.Bar as Baz +import qualified Foo.Quux as Baz +``` + +### Justification + +Explicit export lists are an immediate, clear and obvious indication of what +publically visible interface a module provides. It gives us stability guarantees +(namely, we know we can change things that aren't exported and not break +downstream code at compile time), and tells us where to go looking first when +inspecting or learning the module. Additionally, it means there is less chance +that implementation details 'leak' out of the module due to errors on the part +of developers, especially new developers. + +One of the biggest challenges for modules which depend on other modules +(especially ones that come from the project, rather than an external library) is +knowing where a given identifier's definition can be found. Having explicit +imports of the form described helps make this search as straightforward as +possible. This also limits cognitive load when examining the sources (if we +don't import something, we don't need to care about it in general). Lastly, +being explicit avoids stealing too many useful names. + +In general, type names occur far more often in code than function calls: we have +to use a type name every time we write a type signature, but it's unlikely we +use only one function that operates on said type. Thus, we want to reduce the +amount of extra noise needed to write a type name if possible. Additionally, +name clashes from function names are far more likely than name clashes from type +names: consider the number of types on which a ``size`` function makes sense. +Thus, importing type names unqualified, even if the rest of the module is +qualified, is good practice, and saves on a lot of prefixing. + +## Plutus module import naming conventions + +In addition to the general module import rules, we follow some conventions +on how we import the Plutus API modules, allowing for some flexibility +depending on the needs of a particular module. + +Modules under the names `Plutus`, `Ledger` and `Plutus.V1.Ledger` SHOULD +be imported qualified with their module name, as per the general module standards. +An exception to this is `Plutus.V1.Ledger.Api`, where the `Ledger` name is preferred. + +Some other exceptions to this are allowed where it may be more convenient to +avoid longer qualified names. + +For example: + +```haskell +import Plutus.V1.Ledger.Slot qualified as Slot +import Plutus.V1.Ledger.Tx qualified as Tx +import Plutus.V1.Ledger.Api qualified as Ledger +import Ledger.Oracle qualified as Oracle +import Plutus.Contract qualified as Contract +``` + +In some cases it may be justified to use a shortened module name: + +```haskell +import Plutus.V1.Ledger.AddressMap qualified as AddrMap +``` + +Modules under `PlutusTx` that are extensions to `PlutusTx.Prelude` MAY be +imported unqualified when it is reasonable to do so. + +The `Plutus.V1.Ledger.Api` module SHOULD be avoided in favour of more +specific modules where possible. For example, we should avoid: + +```haskell +import Plutus.V1.Ledger.Api qualified as Ledger (ValidatorHash) +``` + +In favour of: + +```haskell +import Plutus.V1.Ledger.Scripts qualified as Scripts (ValidatorHash) +``` + +### Justification + +The Plutus API modules can be confusing, with numerous modules involved, many +exporting the same items. Consistent qualified names help ease this problem, +and decrease ambiguity about where imported items come from. + +## LANGUAGE pragmata + +The following pragmata MUST be enabled at project level (that is, in +``package.yaml``): + +* ``BangPatterns`` +* ``BinaryLiterals`` +* ``ConstraintKinds`` +* ``DataKinds`` +* ``DeriveFunctor`` +* ``DeriveGeneric`` +* ``DeriveTraversable`` +* ``DerivingStrategies`` +* ``DuplicateRecordFields`` +* ``EmptyCase`` +* ``FlexibleContexts`` +* ``FlexibleInstances`` +* ``GADTs`` +* ``GeneralizedNewtypeDeriving`` +* ``HexFloatLiterals`` +* ``InstanceSigs`` +* ``ImportQualifiedPost`` +* ``KindSignatures`` +* ``LambdaCase`` +* ``MultiParamTypeClasses`` +* ``NoImplicitPrelude`` +* ``NumericUnderscores`` +* ``OverloadedStrings`` +* ``StandaloneDeriving`` +* ``TupleSections`` +* ``TypeApplications`` +* ``TypeOperators`` +* ``TypeSynonymInstances`` +* ``UndecidableInstances`` + +Any other LANGUAGE pragmata MUST be enabled per-file. All language pragmata MUST +be at the top of the source file, written as ``{-# LANGUAGE PragmaName #-}``. + +Furthermore, the following pragmata MUST NOT be used, or enabled, anywhere: + +* ``DeriveDataTypeable`` +* ``DeriveFoldable`` +* ``PartialTypeSignatures`` +* ``PostfixOperators`` + +### Justification + +``DataKinds``, ``DuplicateRecordFields``, ``GADTs``, ``TypeApplications``, +``TypeSynonymInstances`` and ``UndecidableInstances`` are needed globally to use +the GHC plugin from ``record-dot-preprocessor``. While some of these extensions +are undesirable to use globally, we end up needing them anyway, so we can't +really avoid this. + +``BangPatterns`` are a much more convenient way to force evaluation than +repeatedly using `seq`. Furthemore, they're not confusing, and are considered +ubiquitous enough for ``GHC2021``. Having them on by default simplifies a lot of +performance tuning work, and they don't really need signposting. + +``BinaryLiterals``, ``HexFloatLiterals`` and ``NumericUnderscores`` all simulate +features that are found in many other programming languages, and that are +extremely convenient in a range of settings, ranging from dealing with large +numbers to bit-twiddling. If anything, it is more surprising and annoying when +these _aren't_ enabled, and should really be part of Haskell syntax anyway. +Enabling this project-wide actually encourages better practice and readability. + +The kind ``Constraint`` is not in Haskell2010, and thus, isn't recognized by +default. While working with constraints as first-class objects isn't needed +often, this extension effectively exists because Haskell2010 lacks exotic kinds +altogether. Since we require explicit kind signatures (and foralls) for all type +variables, this needs to be enabled as well. There is no harm in enabling this +globally, as other rich kinds (such as ``Symbol`` or ``Nat``) don't require an +extension for their use, and this doesn't change any behaviour (``Constraint`` +exists whether you enable this extension or not, as do 'exotic kinds' in +general). + +``DerivingStrategies`` is good practice (and in fact, is mandated by this +document); it avoids ambiguities between ``GeneralizedNewtypeDeriving`` and +``DeriveAnyClass``, allows considerable boilerplate savings through use of +``DerivingVia``, and makes the intention of the derivation clear on immediate +reading, reducing the amount of non-local information about derivation +priorities that we have to retain. ``DeriveFunctor`` and +``GeneralizedNewtypeDeriving`` are both obvious and useful extensions to the +auto-derivation systems available in GHC. Both of these have only one correct +derivation (the former given by [parametricity +guarantees][functor-parametricity], the latter by the fact that a newtype only +wraps a single value). As there is no chance of unexpected behaviour by these, +no possible behaviour variation, and that they're key to supporting both the +``stock`` and ``newtype`` deriving stratgies, having these on by default removes +considerable tedium and line noise from our code. A good example are newtype +wrappers around monadic stacks: + +```haskell +newtype FooM a = FooM (ReaderT Int (StateT Text IO) a) + deriving newtype ( + Functor, + Applicative, + Monad, + MonadReader Int, + MonadState Text, + MonadIO + ) +``` + +Deriving ``Traversable`` is a little tricky. While ``Traversable`` is lawful +(though not to the degree ``Functor`` is, permitting multiple implementations in +many cases), deriving it is complicated by issues of role assignation for +higher-kinded type variables and the fact that you can't ``coerce`` through a +``Functor``. These are arguably implementation issues, but repairing this +situation requires cardinal changes to ``Functor``, which is unlikely to ever +happen. Even newtype or via derivations of ``Traversable`` are mostly +impossible; thus, we must have special support from GHC, which +``DeriveTraversable`` enables. This is a very historically-motivated +inconsistency, and should really not exist at all. While this only papers over +the problem (as even with this extension on, only stock derivations become +possible), it at least means that it can be done at all. Having it enabled +globally makes this inconsistency slightly less visible, and is completely safe. + +While GHC ``Generic``s are far from problem-free, many parts of the Haskell +ecosystem require ``Generic``, either as such (c.f. ``beam-core``) or for +convenience (c.f ``aeson``, ``hashable``). Additionally, several core parts of +Plutus (including ``ToSchema``) are driven by ``Generic``. The derivation is +trivial in most cases, and having to enable an extension for it is quite +annoying. Since no direct harm is done by doing this, and use of ``Generic`` is +already signposted clearly (and is mostly invisible), having this on globally +poses no problems. + +``EmptyCase`` not being on by default is an inconsistency of Haskell 2010, as +the report allows us to define an empty data type, but without this extension, +we cannot exhaustively pattern match on it. This should be the default behaviour +for reasons of symmetry. + +``FlexibleContexts`` and ``FlexibleInstances`` paper over a major deficiency of +Haskell2010, which in general isn't well-motivated. There is no real reason to +restrict type arguments to variables in either type class instances or type +signatures: the reasons for this choice in Haskell2010 are entirely for the +convenience of the implementation. It produces no ambiguities, and in many ways, +the fact this _isn't_ the default is more surprising than anything. +Additionally, many core libraries rely on one, or both, of these extensions +being enabled (``mtl`` is the most obvious example, but there are many others). +Thus, even for popularity and compatibility reasons, these should be on by +default. + +``InstanceSigs`` are harmless by default, and introduce no complications. Their +not being default is strange. ``ImportQualifiedPost`` is already a convention +of this project, and helps with formatting of imports. + +``KindSignatures`` become extremely useful in any setting where 'exotic kinds' +(meaning, anything which isn't `Type` or `Type -> Type` or similar) are +commonplace; much like type signatures clarify expectations and serve as active +documentation (even where GHC can infer them), explicit kind signatures serve +the same purpose 'one level up'. When combined with the requirement to provide +explicit foralls for type variables defined in this document, they simplify the +usage of 'exotic kinds' and provide additional help from both the type checker +and the code. Since this project is Plutus-based, we use 'exotic kinds' +extensively, especially in row-polymorphic records; thus, in our case, this is +especially important. This also serves as justification for +`ScopedTypeVariables`, as well as ironing out a weird behaviour where in cases +such as + +```haskell +foo :: a -> b +foo = bar . baz + where + bar :: String -> b + bar = ... + baz :: a -> String + baz = ... +``` + +cause GHC to produce _fresh_ type variables in each ``where``-bind. This is +confusing and makes little sense - if the user wanted a fresh variable, they +would name it that way. What's worse is that the type checker emits an error +that makes little sense (except to those who have learned to look for this +error), creating even more confusion, especially in cases where the type +variable is constrained: + +```haskell +foo :: (Monoid m) => m -> String +foo = bar . baz + where + baz :: m -> Int + baz = ... -- this has no idea that m is a Monoid, since m is fresh! +``` + +``LambdaCase`` reduces a lot of code in the common case of analysis of sum +types. Without it, we are forced to either write a dummy ``case`` argument: + +```haskell +foo s = case s of +-- rest of code here +``` + +Or alternatively, we need multiple heads: + +```haskell +foo Bar = -- rest of code +foo (Baz x y) = -- rest of code +-- etc +``` + +``LambdaCase`` is shorter than both of these, and avoids us having to bind +variables, only to pattern match them away immediately. It is convenient, clear +from context, and really should be part of the language to begin with. + +``MultiParamTypeClasses`` are required for a large number of standard Haskell +libraries, including ``mtl`` and ``vector``, and in many situations. Almost any +project of non-trivial size must have this extension enabled somewhere, and if +the code makes significant use of ``mtl``-style monad transformers or defines +anything non-trivial for ``vector``, it must use it. Additionally, it arguably +lifts a purely implementation-driven decision of the Haskell 2010 language, much +like ``FlexibleContexts`` and ``FlexibleInstances``. Lastly, although it can +introduce ambiguity into type checking, it only applies when we want to define +our own multi-parameter type classes, which is rarely necessary. Enabling it +globally is thus safe and convenient. + +Based on the recommendations of this document (driven by the needs of the +project and the fact it's cardinally connected with Plutus), +``NoImplicitPrelude`` is required to allow us to default to the Plutus prelude +instead of the one from ``base``. + +``OverloadedStrings`` deals with the problem that ``String`` is a suboptimal +choice of string representation for basically _any_ problem, with the general +recommendation being to use ``Text`` instead. It is not, however, without its +problems: + +* ``ByteString``s are treated as ASCII strings by their ``IsString`` instance; +* Overly polymorphic behaviour of many functions (especially in the presence of + type classes) forces extra type signatures; + +These are usually caused not by the extension itself, but by other libraries and +their implementations of either ``IsString`` or overly polymorphic use of type +classes without appropriate laws (Aeson's ``KeyValue`` is a particularly +egregious offender here). The convenience of this extension in the presence of +literals, and the fact that our use cases mostly covers ``Text``, makes it worth +using by default. + +``StandaloneDeriving`` is mostly needed for GADTs, or situations where complex +type-level computations drive type class instances, requiring users to specify +constraints manually. This can pose some difficulties syntactically (such as +with deriving strategies), but isn't a problem in and of itself, as it doesn't +really change how the language works. Having this enabled globally is not +problematic. + +``TupleSections`` smooths out an oddity in the syntax of Haskell 2010 regarding +partial application of tuple constructors. Given a function like ``foo :: Int -> String -> +Bar``, we accept it as natural that we can write ``foo 10`` to get a function of +type ``String -> Bar``. However, by default, this logic doesn't apply to tuple +constructors. As special cases are annoying to keep track of, and in this case, +serve no purpose, as well as being clear from their consistent use, this should +also be enabled by default; it's not clear why it isn't already. + +``TypeOperators`` is practically a necessity when dealing with type-level +programming seriously. Much how infix data constructors are extremely useful +(and sometimes clearer than their prefix forms), infix _type_ constructors serve +a similar functionality. Additionally, Plutus relies on operators at the type +level significantly - for example, it's not really possible to define a +row-polymorphic record or variant without them. Having to enable this almost +everywhere is a needless chore, and having type constructors behaving +differently to data constructors here is a needless source of inconsistency. + +We exclude ``DeriveDataTypeable``, as ``Data`` is a strictly-worse legacy +version of ``Generic``, and ``Typeable`` no longer needs deriving for anything +anyway. The only reason to derive either of these is for compatibility with +legacy libraries, which we don't have any of, and the number of which shrinks +every year. If we're using this extension at all, it's probably a mistake. + +``Foldable`` is possibly the most widely-used lawless type class. Its only laws +are about self-consistency (such as agreement between ``foldMap`` and +``foldr``), but unlike something like ``Functor``, ``Foldable`` doesn't have any +laws specifying its behaviour outside of 'it compiles'. As a result, even if we +accept its usefulness (a debatable position in itself), there are large numbers +of possible implementations that could be deemed 'valid'. The approach taken by +``DeriveFoldable`` is _one_ such approach, but this requires knowing its +derivation algorithm, and may well not be something you need. Unlike a +``Functor`` derivation (whose meaning is obvious), a ``Foldable`` one is +anything but, and requires referencing a lot of non-local information to +determine how it will behave (especially for the 'richer' ``Foldable``, with +many additional methods). If you need a ``Foldable`` instance, you will either +newtype or via-derive it (which doesn't need this extension anyway), or you'll +write your own (which _also_ doesn't need this extension). Enabling this +encourages bad practices, is confusing, and ultimately doesn't really benefit +anything. + +``PartialTypeSignatures`` is a misfeature. Allowing leaving in type holes (to be +filled by GHC's inference algorithm) is an anti-pattern for the same reason that +not providing top-level signatures: while it's possible (mostly) for GHC to +infer signatures, we lose considerable clarity and active documentation by doing +so, in return for (quite minor) convenience. While the use of typed holes during +development is a good practice, they should not remain in final code. Given that +Plutus projects require us to do some fairly advanced type-level programming +(where inference often _fails_), this extension can often provide totally +incorrect results due to GHC's 'best-effort' attempts at type checking. There is +no reason to leave behind typed holes instead of filling them in, and we +shouldn't encourage this. + +``PostfixOperators`` are arguably a misfeature. Infix operators already require +a range of special cases to support properly (what symbols create an infix +operator, importing them at the value and type level, etc), which postfix +operators make _worse_. Furthermore, they are seldom, if ever, used, and +typically aren't worth the trouble. Haskell is not Forth, none of our +dependencies rely on postfix operators, and defining our own creates more +problems than it solves. + +## ``record-dot-preprocessor`` + +The GHC plugin from ``record-dot-preprocessor`` SHOULD be enabled globally. + +### Justification + +Haskell records are documentedly and justifiably subpar: the [original issue for +the record dot preprocessor extension][rdp-issue] provides a good summary of the +reasons. While a range of extensions (including ``DuplicateRecordFields``, +``DisambiguateRecordFields``, ``NamedFieldPuns``, and many others) have been +proposed, and accepted, to mitigate the situation, the reality is that, even +with them in place, use of records in Haskell is considerably more difficult, +and less flexible, than in any other language in widespread use today. The +proposal described in the previous link provides a solution which is familiar to +users of most other languages, and addresses the fundamental issue that makes +Haskell records so awkward. + +While the proposal for the record dot syntax that this preprocessor enables is +coming, it's not available in the current version of Haskell used by Plutus (and +thus, transitively, by us). Additionally, the earliest this will be available is +GHC 9.2, and given that our dependencies must support this version too, it'll be +considerable time before we can get its benefits. The preprocessor gives us +these benefits immediately, at some dependency cost. While it's not a perfect +process, as it involves enabling several questionable extensions, and can +require disabling an important warning, it significantly reduces issues with +record use, making it worthwhile. Additionally, when GHC 9.2 becomes usable, we +can upgrade to it seamlessly. + +## Prelude + +The ``PlutusTx.Prelude`` MUST be used. A 'hiding import' to remove functionality +we want to replace SHOULD be used when necessary. If functionality from the +``Prelude`` in ``base`` is needed, it SHOULD be imported qualified. Other +preludes MUST NOT be used. + +### Justification + +As this is primarily a Plutus project, we are in some ways limited by what +Plutus requires (and provides). Especially for on-chain code, the Plutus prelude +is the one we need to use, and therefore, its use should be as friction-free as +possible. As many modules may contain a mix of off-chain and on-chain code, we +also want to make impendance mismatches as limited as possible. + +By the very nature of this project, we can assume a familiarity (or at least, +the goal of such) with Plutus stuff. Additionally, _every_ Haskell developer is +familiar with the ``Prelude`` from ``base``. Thus, any replacements of the +Plutus prelude functionality with the ``base`` prelude should be clearly +indicated locally. + +Haskell is a 30-year-old language, and the ``Prelude`` is one of its biggest +sources of legacy. A lot of its defaults are questionable at best, and often +need replacing. As a consequence of this, a range of 'better ``Prelude``s' have +been written, with a range of opinions: while there is a common core, a large +number of decisions are opinionated in ways more appropriate to the authors of +said alternatives and their needs than those of other users of said +alternatives. This means that, when a non-``base`` ``Prelude`` is in scope, it +often requires familiarity with its specific decisions, in addition to whatever +cognitive load the current module and its other imports impose. Given that we +already use an alternative prelude (in tandem with the one from ``base``), +additional alternatives present an unnecessary cognitive load. Lastly, the +dependency footprint of many alternative ``Prelude``s is _highly_ non-trivial; +it isn't clear if we need all of this in our dependency tree. + +For all of the above reasons, the best choice is 'default to Plutus, with local +replacements from `base`'. + +## Versioning + +A project MUST use the [PVP][pvp]. Two, and only two, version numbers MUST be +used: a major version and a minor version. + +### Justification + +The [Package Versioning Policy][pvp] is the conventional Haskell versioning +scheme, adopted by most packages on Hackage. It is clearly described, and even +automatically verifiable by use of tools like [``policeman``][policeman]. Thus, +adopting it is both in line with community standards (making it easier to +remember), and simplifies cases such as Hackage publication or open-sourcing in +general. + +Two version numbers (major and minor) is the minimum allowed by the PVP, +indicating compilation-breaking and compilation-non-breaking changes +respectively. As parsimony is best, and more granularity than this isn't +generally necessary, adopting this model is the right decision. + +## Documentation + +Every publically-exported definition MUST have a Haddock comment, detailing its +purpose. If a definition is a function, it SHOULD also have examples of use +using [Bird tracks][bird-tracks]. The Haddock for a publically-exported +definition SHOULD also provide an explanation of any caveats, complexities of +its use, or common issues a user is likely to encounter. + +If the code project is a library, these Haddock comments SHOULD carry an +[``@since``][haddock-since] annotation, stating what version of the library they +were introduced in, or the last version where their functionality or type +signature changed. + +For type classes, their laws MUST be documented using a Haddock comment. + +### Justification + +Code reading is a difficult task, especially when the 'why' rather than the +'how' of the code needs to be deduced. A good solution to this is documentation, +especially when this documentation specifies common issues, provides examples of +use, and generally states the rationale behind the definition. + +For libraries, it is often important to inform users what changed in a given +version, especially where 'major bumps' are concerned. While this would ideally +be addressed with accurate changelogging, it can be difficult to give proper +context. ``@since`` annotations provide a granular means to indicate the last +time a definition changed considerably, allowing someone to quickly determine +whether a version change affects something they are concerned with. + +As stated elsewhere in the document, type classes having laws is critical to our +ability to use equational reasoning, as well as a clear indication of what +instances are and aren't permissible. These laws need to be clearly stated, as +this assists both those seeking to understand the purpose of the type class, and +also the expected behaviour of its instances. + +## Other + +Lists SHOULD NOT be field values of types; this extends to ``String``s. Instead, +``Vector``s (``Text``s) SHOULD be used, unless a more appropriate structure exists. +On-chain code, due to a lack of alternatives, is one place lists can be used as +field values of types. + +Partial functions MUST NOT be defined. Partial functions SHOULD NOT be used +except to ensure that another function is total (and the type system cannot be +used to prove it). + +Derivations MUST use an explicit [strategy][deriving-strategies]. Thus, the +following is wrong: + +```haskell +newtype Foo = Foo (Bar Int) + deriving (Eq, Show, Generic, FromJSON, ToJSON, Data, Typeable) +``` + +Instead, write it like this: + +```haskell +newtype Foo = Foo (Bar Int) + deriving stock (Generic, Data, Typeable) + deriving newtype (Eq, Show) + deriving anyclass (FromJSON, ToJSON) +``` + +Deriving via SHOULD be preferred to newtype derivation, especially where the +underlying type representation could change significantly. + +``type`` SHOULD NOT be used. The only acceptable case is abbreviation of large +type-level computations. In particular, using ``type`` to create an abstraction +boundary MUST NOT be done. + +Type variables MUST have an explicit ``forall`` scoping it, and all type +variables MUST have kind signatures explicitly provided. Thus, the following is +wrong: + +```haskell +data Foo a = Bar | Baz [a] + +quux :: (Monoid m) => [m] -> m -> m +``` + +Instead, write it like this: + +```haskell +data Foo (a :: Type) = Bar | Baz [a] + +quux :: forall (m :: Type) . (Monoid m) => [m] -> m -> m +``` + +`where`-bindings MUST have type signatures. + +### Justification + +Haskell lists are a large example of the legacy of the language: they (in the +form of singly linked lists) have played an important role in the development of +functional programming (and for some 'functional' languages, continue to do so). +However, from the perspective of data structures, they are suboptimal except for +_extremely_ specific use cases. In almost any situation involving data (rather +than control flow), an alternative, better structure exists. Although it is both +acceptable and efficient to use lists within functions (due to GHC's extensive +fusion optimizations), from the point of view of field values, they are a poor +choice from both an efficiency perspective, both in theory _and_ in practice. +For almost all cases where you would want a list field value, a ``Vector`` field +value is more appropriate, and in almost all others, some other structure (such +as a ``Map``) is even better. We make a named exception for on-chain code, as no +alternatives presently exist. + +Partial functions are runtime bombs waiting to explode. The number of times the +'impossible' happened, especially in production code, is significant in our +experience, and most partiality is easily solvable. Allowing the compiler to +support our efforts, rather than being blind to them, will help us write more +clear, more robust, and more informative code. Partiality is also an example of +legacy, and it is legacy of _considerable_ weight. Sometimes, we do need an +'escape hatch' due to the impossibility of explaining what we want to the +compiler; this should be the _exception_, not the rule. + +Derivations are one of the most useful features of GHC, and extend the +capabilities of Haskell 2010 considerably. However, with great power comes great +ambiguity, especially when ``GeneralizedNewtypeDeriving`` is in use. While there +_is_ an unambiguous choice if no strategy is given, it becomes hard to remember. +This is especially dire when ``GeneralizedNewtypeDeriving`` combines with +``DeriveAnyClass`` on a newtype. Explicit strategies give more precise control +over this, and document the resulting behaviour locally. This reduces the number +of things we need to remember, and allows more precise control when we need it. +Lastly, in combination with ``DerivingVia``, considerable boilerplate can be +saved; in this case, explicit strategies are _mandatory_. + +The only exception to the principle above is newtype deriving, which can +occasionally cause unexpected problems; if we use a newtype derivation, and +change the underlying type, we get no warning. Since this can affect the effect +of some type classes drastically, it would be good to have the compiler check +our consistency. + +``type`` is generally a terrible idea in Haskell. You don't create an +abstraction boundary with it (any operations on the 'underlying type' still work +over it), and compiler output becomes _very_ inconsistent (sometimes showing the +``type`` definition, sometimes the underlying type). If your goal is to create +an abstraction boundary with its own operations, ``newtype`` is both cost-free +and clearer; if that is _not_ your goal, just use the type you'd otherwise +rename, since it's equivalent semantically. The only reasonable use of ``type`` +is to hide complex type-level computations, which would otherwise be too long. +Even this is somewhat questionable, but the questionability comes from the +type-level computation being hidden, not ``type`` as such. + +Type-level programming is mandated in many places by Plutus (including, but not +limited to, row-polymorphic records and variants from `Data.Row`). This often +requires use of ``TypeApplications``, which essentially makes not only the type +variables, but their _order_, part of the API of any definition that uses them. +While there is an algorithm determining this precisely, something that is +harmless at the value level (such as re-ordering constraints) could potentially +serve as an API break. Additionally, this algorithm is a huge source of +non-local information, and in the presence of a large number of type variables, +of different kinds, can easily become confusing. Having explicit foralls +quantifying all type variables makes it clear what the order for these type +variables is for ``TypeApplications``, and also allows us to choose it +optimally for our API, rather than relying on what the algorithm would produce. +This is significantly more convenient, and means less non-local information and +confusion. + +Additionally, type-level programming requires significant use of 'exotic kinds', +which in our case include ``Constraint -> Type`` and ``Row Type``, to name but a +few. While GHC can (mostly) infer kind signatures, much the same way as we +explicitly annotate type signatures as a form of active documentation (and to +assist the type checker when using type holes), explicitly annotating _kind_ +signatures allows us to be clear to the users where exotic kinds are expected, +as well as ensuring that we don't make any errors ourselves. This, together with +explicit foralls, essentially bring the same practices to the kind level as the +Haskell community already considers to be good at the type level. + +`where` bindings are quite common in idiomatic Haskell, and quite often contain +non-trivial logic. They're also a common refactoring, and 'hole-driven +development' tool, where you create a hole to be filled with a `where`-bound +definition. Even in these cases, having an explicit signature on +`where`-bindings helps: during development, you can use typed holes inside the +`where`-binding with useful information (absent a signature, you'll get +nothing), and it makes the code much easier to understand, especially if the +`where`-binding is complex. It's also advantageous when 'promoting' +`where`-binds to full top-level definitions, as the signature is already there. +Since we need to do considerable type-level programming as part of Plutus, this +becomes even more important, as GHC's type inference algorithm can often fail in +those cases on `where`-bindings, which will sometimes fail to derive, giving a +very strange error message, which would need a signature to solve anyway. By +making this practice proactive, we are decreasing confusion, as well as +increasing readability. While in theory, this standard should extend to +`let`-bindings as well, these are much rarer, and can be given signatures with +`::` if `ScopedTypeVariables` is on (which it is for us by default) if needed. + +# Design practices + +## Parse, don't validate + +[Boolean blindness][boolean-blindness] SHOULD NOT be used in the design of any +function or API. Returning more meaningful data SHOULD be the preferred choice. +The general principle of ['parse, don't validate'][parse-dont-validate] SHOULD +guide design and implementation. + +### Justification + +The [description of boolean blindness][boolean-blindness] gives specific reasons why it is a poor +design choice; additionally, it runs counter to the principle of ['parse, don't +validate][parse-dont-validate]. While sometimes unavoidable, in many cases, it's +possible to give back a more meaningful response than 'yes' or 'no, and we +should endeavour to do this. Designs that avoid boolean blindness are more +flexible, less bug-prone, and allow the type checker to assist us when writing. +This, in turn, reduces cognitive load, improves our ability to refactor, and +means fewer bugs from things the compiler _could_ have checked if a function +_wasn't_ boolean-blind. + +## No multi-parameter type-classes without functional dependencies + +Any multi-parameter type class MUST have a functional dependency restricting its +relation to a one-to-many at most. In cases of true many-to-many relationships, +type classes MUST NOT be used as a solution to the problem. + +### Justification + +Multi-parameter type classes allow us to express more complex relationships +among types; single-parameter type classes effectively permit us to 'subset' +``Hask`` only. However, multi-parameter type classes make type inference +_extremely_ flakey, as the global coherence condition can often lead to the +compiler being unable to determine what instance is sought even if all the type +parameters are concrete, due to anyone being able to add a new instance at any +time. This is largely caused by multi-parameter type classes defaulting to +effectively representing arbitrary many-to-many relations. + +When we do not _have_ arbitrary many-to-many relations, multi-parameter type +classes are useful and convenient. We can indicate this using functional +dependencies, which inform the type checker that our relationship is not +arbitrarily many-to-many, but rather many-to-one or even one-to-one. This is a +standard practice in many libraries (``mtl`` being the most ubiquitous example), +and allows us the benefits of multi-parameter type classes without making type +checking confusing and difficult. + +In general, many-to-many relationships pose difficult design choices, for which +type classes are _not_ the correct solution. If a functional dependency _cannot_ +be provided for a type class, it suggests that the current design relies +inherently on a many-to-many relation, and should be either rethought to +eliminate it, or be dealt with using a more appropriate means. + +## Type classes must have laws + +Any type class not imported from an external dependency MUST have laws. These +laws MUST be documented in a Haddock comment on the type class definition, and +all instances MUST follow these laws. + +### Justification + +Type classes are a powerful feature of Haskell, but can also be its most +confusing. As they allow arbitrary ad-hoc polymorphism, and are globally +visible, it is important that we limit the confusion this can produce. +Additionally, type classes without laws inhibit equational reasoning, which is +one of Haskell's biggest strengths, _especially_ in the presence of what amounts +to arbitrary ad-hoc polymorphism. + +Additionally, type classes with laws allow the construction of _provably_ +correct abstractions above them. This is also a common feature in Haskell, +ranging from profunctor optics to folds. If we define our own type classes, we +want to be able to abstract above them with _total_ certainty of correctness. +Lawless type classes make this difficult to do: compare the number of +abstractions built on `Functor` or `Traversable` as opposed to `Foldable`. + +Thus, type classes having laws provides both ease of understanding and +additional flexibility. + +[pvp]: https://pvp.haskell.org/ +[policeman]: https://hackage.haskell.org/package/policeman +[haddock-since]: https://haskell-haddock.readthedocs.io/en/latest/markup.html#since +[bird-tracks]: https://haskell-haddock.readthedocs.io/en/latest/markup.html#code-blocks +[hedgehog-classes]: http://hackage.haskell.org/package/hedgehog-classes +[hspec-hedgehog]: http://hackage.haskell.org/package/hspec-hedgehog +[property-based-testing]: https://dl.acm.org/doi/abs/10.1145/1988042.1988046 +[hedgehog]: http://hackage.haskell.org/package/hedgehog +[deriving-strategies]: https://gitlab.haskell.org/ghc/ghc/-/wikis/commentary/compiler/deriving-strategies +[functor-parametricity]: https://www.schoolofhaskell.com/user/edwardk/snippets/fmap +[alexis-king-options]: https://lexi-lambda.github.io/blog/2018/02/10/an-opinionated-guide-to-haskell-in-2018/#warning-flags-for-a-safe-build +[hlint]: http://hackage.haskell.org/package/hlint +[fourmolu]: http://hackage.haskell.org/package/fourmolu +[rfc-2119]: https://tools.ietf.org/html/rfc2119 +[boolean-blindness]: http://dev.stephendiehl.com/hask/#boolean-blindness +[parse-dont-validate]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ +[hspec]: http://hackage.haskell.org/package/hspec +[rdp]: https://hackage.haskell.org/package/record-dot-preprocessor +[rdp-issue]: https://github.com/ghc-proposals/ghc-proposals/pull/282 diff --git a/flake.lock b/flake.lock new file mode 120000 index 000000000..654d51086 --- /dev/null +++ b/flake.lock @@ -0,0 +1 @@ +mlabs/flake.lock \ No newline at end of file diff --git a/flake.nix b/flake.nix new file mode 120000 index 000000000..0edbe14a4 --- /dev/null +++ b/flake.nix @@ -0,0 +1 @@ +mlabs/flake.nix \ No newline at end of file diff --git a/mlabs/.gitattributes b/mlabs/.gitattributes new file mode 100644 index 000000000..af4fe8b58 --- /dev/null +++ b/mlabs/.gitattributes @@ -0,0 +1 @@ +flake.lock linguist-generated=true diff --git a/mlabs/.gitignore b/mlabs/.gitignore new file mode 100644 index 000000000..e10810426 --- /dev/null +++ b/mlabs/.gitignore @@ -0,0 +1,15 @@ +dist-newstyle/ +.stack-work/ +demo-frontend/.spago +demo-frontend/output +demo-frontend/node_modules +demo-frontend/.cache +demo-frontend/dist +stack.yaml.lock +result* +*~ +*# +plutus-pab.yaml +pab-core.db +.direnv/ +.envrc diff --git a/mlabs/CHANGELOG.md b/mlabs/CHANGELOG.md new file mode 100644 index 000000000..10169f277 --- /dev/null +++ b/mlabs/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for mlabs + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/mlabs/LICENSE b/mlabs/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/mlabs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/mlabs/Makefile b/mlabs/Makefile new file mode 100644 index 000000000..6b2bf7dda --- /dev/null +++ b/mlabs/Makefile @@ -0,0 +1,61 @@ +.PHONY: build-nix hoogle nix-build-library nix-build-executables \ + nix-build-test nix-cabal-repl requires_nix_shell ci-build-run + +# Generate TOC for README.md +# It has to be manually inserted into the README.md for now. +generate-readme-contents: + nix shell nixpkgs#nodePackages.npm --command "npx markdown-toc ./README.md --no-firsth1" + +# Starts a hoogle Server. +hoogle: + @ nix develop -c hoogle server --local --port 8008 + +# Attempt the CI locally +ci-build-run: + @ ./run-tests.sh + +# Build the library with nix. +nix-build-library: + @ nix build .#mlabs-plutus-use-cases:lib:mlabs-plutus-use-cases + +current-system := $(shell nix eval --impure --expr builtins.currentSystem) + +# Build the executables with nix (also builds the test suite). +nix-build-executables: + @ nix build .#check.${current-system} + +# Build the tests with nix. +nix-build-test: + @ nix build .#mlabs-plutus-use-cases:test:mlabs-plutus-use-cases-tests + +# Starts a ghci repl inside the nix environment. +nix-cabal-repl: + @ nix develop -c cabal new-repl + +# Target to use as dependency to fail if not inside nix-shell. +requires_nix_shell: + @ [ "($IN_NIX_SHELL)" ] || echo "The $(MAKECMDGOALS) target must be run from inside `nix develop`" + @ [ "($IN_NIX_SHELL)" ] || (echo " run `nix develop` first" && false) + +# Build with Stack - independent of NIX. +stack-build: + stack build --ghc-options="-Wall" + +# Test with Stack. +stack-test: + stack test all + +# Watch with Stack. +stack-watch: + stack build --file-watch --ghc-options="-Wall" + +# Watch Test with Stack. +stack-test-watch: + stack test --file-watch + +# Add folder locations to the list to be reformatted. +fourmolu-format: + @ echo "> Formatting all .hs files" + fourmolu -i $$(find src/ -iregex ".*.hs") + fourmolu -i $$(find test/ -iregex ".*.hs") + fourmolu -i $$(find app/ -iregex ".*.hs") diff --git a/mlabs/README.md b/mlabs/README.md new file mode 100644 index 000000000..ba94c5b7c --- /dev/null +++ b/mlabs/README.md @@ -0,0 +1,337 @@ +# MLabs: Plutus Use Cases + +-------------------------------------------------------------------------------- + +## Status of Main + +[![Build](https://github.com/mlabs-haskell/plutus-use-cases/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/mlabs-haskell/plutus-use-cases/actions/workflows/build.yml) + +[![Lint](https://github.com/mlabs-haskell/plutus-use-cases/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/mlabs-haskell/plutus-use-cases/actions/workflows/lint.yml) + +[![Formatting](https://github.com/mlabs-haskell/plutus-use-cases/actions/workflows/formatting.yml/badge.svg?branch=main)](https://github.com/mlabs-haskell/plutus-use-cases/actions/workflows/formatting.yml) + +## Contents + +- [MLabs: Plutus Use Cases](#mlabs-plutus-use-cases) + - [Contents](#contents) + - [Overview](#overview) + - [Prerequisites](#prerequisites) + - [Building, Testing, Use](#building-testing-use) + - [On Unix Systems](#on-unix-systems) + - [Documentation](#documentation) + - [Testing](#testing) + - [Running Tests](#running-tests) + - [Use Case: Lendex](#use-case-lendex) + - [Lendex: Description](#lendex-description) + - [Lendex: Progress & Planning](#lendex-progress--planning) + - [Lendex: Examples](#lendex-examples) + - [Lendex: APIs & Endpoints](#lendex-apis--endpoints) + - [Lendex: Tests](#lendex-tests) + - [Lendex: Notes](#lendex-notes) + - [Use Case: NFT](#use-case-nft) + - [NFT: Description](#nft-description) + - [NFT: Progress & Planning](#nft-progress--planning) + - [NFT: Examples](#nft-examples) + - [NFT: APIs & Endpoints](#nft-apis--endpoints) + - [NFT: Tests](#nft-tests) + - [NFT: Notes](#nft-notes) + +*note: the table of contents is generated using `make readme_contents`, please +update as headings are expanded.* + +-------------------------------------------------------------------------------- + +## Overview + +MLabs has been working on developing two Plutus Use cases, specifically: + +- [Use Case: Lendex](#use-case-lendex) based on the specification of + [Plutus Use case 3](https://github.com/mlabs-haskell/plutus-use-cases/tree/documentation#use-case-3-lending-and-borrowing-collateral-escrow-flashloans). + +- [Use Case: NFT](#use-case-nft) based on the specification of + [Plutus Use case 5](https://github.com/mlabs-haskell/plutus-use-cases/tree/documentation#use-case-5-nfts-minting-transfer-buying-and-selling-nfts). + +Please refer to each individual Plutus Use Case for more specific information. + +### Prerequisites + +- Git +- Curl +- Nix + +### Building, Testing, Use + +# HLS setup (tested for Visual Studio Code) +Start editor from nix-shell. Let the editor find the correct version of haskell-language-server binary. +#### On Unix Systems + +*It is recommended that all current updates to your system be done before +installation* + +1) ***Install basic dependencies*** + +```bash +sudo apt install curl +sudo apt install git +``` + +2) ***Clone Directory*** + +Create a directory and clone the project: + +```bash +git clone https://github.com/mlabs-haskell/plutus-use-cases.git +``` + +3) ***Install Nix*** + + 1) **Setup Nix** + + ```bash + $ curl -L https://nixos.org/nix/install | sh + ``` + - *There is a issue with nix correctly adjusting the PATH in some machines. + Please re-start your terminal and make sure Nix is in the path (`nix --version`). + See this discussion if you are having this issue: [https://github.com/NixOS/nix/issues/3317](https://github.com/NixOS/nix/issues/3317).* + + - *The direct link to the nix download page for reference: [https://nixos.org/download.html](https://nixos.org/download.html).* + + 2) **Set up binary cache** + + **note: Make sure to set up the IOHK binary cache. If you do not do this, you + will end up building GHC, which takes several hours. If you find + yourself building GHC, STOP and fix the cache.** + + - To set up the binary cache: + + * On **non-NixOS** machines: + + Create a nix directory and file in the `etc` directory. + + 1) `sudo mkdir /etc/nix` + + 2) `sudo touch /etc/nix/nix.conf` + + *Then edit your `nix.conf` file to add:* + + `substituters = https://hydra.iohk.io https://iohk.cachix.org https://cache.nixos.org/` + `trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= iohk.cachix.org-1:DpRUyj7h7V830dp/i6Nti+NEO2/nhblbov/8MW7Rqoo= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=` + + + * On **NixOS** Machines, add the following NixOs options: + + `nix = { + binaryCaches = [ "https://hydra.iohk.io" "https://iohk.cachix.org" ];` + `binaryCachePublicKeys = [ "hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=" "iohk.cachix.org-1:DpRUyj7h7V830dp/i6Nti+NEO2/nhblbov/8MW7Rqoo=" ]; + };` + +Please see the original documentation at IOHK for reference: +- [How to set up the IOHK binary caches](https://github.com/input-output-hk/plutus/blob/master/README.adoc#iohk-binary-cache) + +4) ***Create nix shell*** + +Go to the `plutus-use-cases/mlabs` directory run the `nix-shell` command: +```bash + $ nix-shell +``` +- *note: This will take some time on the first run, as the dependencies get built locally.* + +### Documentation +Currently the documentation is done via this document which can +be found in the [MLabs gitHub repository](https://github.com/mlabs-haskell/plutus-use-cases/tree/main/mlabs) + +### Testing +For an overview of the test coverage and implementation refer to the individual +cases documentation and the [test folder](https://github.com/mlabs-haskell/plutus-use-cases/tree/main/mlabs/test). + +#### Running Tests +*TODO: Add the explanation of how to run tests* + +-------------------------------------------------------------------------------- + +## Use Case: Lendex + +### Lendex: Description + +The Lendex Use Case is based on the Open Source, Non-Custodial Aave Protocol, +described in the [Aave Protocol +Whitepaper](https://github.com/aave/aave-protocol/blob/master/docs/Aave_Protocol_Whitepaper_v1_0.pdf). +The use case can be summarised as a platform for a decentralised, pool-based, +loan strategy. + +As described in the whitepaper, the model relies on Lenders depositing (Cardano) +cryptocurrency in a Pool Contract. The same Pool Contract provides a source for +funds to be borrowed by Borrowers through the placement of a collateral. Loans do +not need to be individually matched, but rather rely on the pooled funds, the +amounts borrowed and their respective collateral. The model enables instant +loans and the interest rate for both borrowers and lenders is decided +algorithmically. A general description of the interest algorithm is: + +- Borrower's interest is tied to the amount of funds available in the pool at + a specific time - with scarcity of funds driving the interest rate up. +- Lender's interest rate corresponds to the earn rate, with the algorithm + safeguarding a liquidity reserve to guarantee ease of withdrawals at any + given time. + +### Lendex: Progress & Planning + +- Goals and status: + - Development + - [x] Feature Completeness as per Specifications + - [ ] Improve Deployment Story + - [ ] Improve Performance + - [ ] Improve Ergonomics of Use and Installation + + - Testing + - [x] 100% Test Coverage + - [ ] QuickCheck Testing + + - Documentation + - [x] Example + - [ ] APIs + +### Lendex: Examples + +- [Lendex Demo](https://github.com/mlabs-haskell/plutus-use-cases/blob/main/mlabs/lendex-demo/Main.hs) + - to run the `lendex-demo` run the following command from the root folder: + + ```bash + cd mlabs \ + && nix-shell --command "cabal v2-repl lendex-demo" + ``` + +Are defined in [mlabs/src/Mlabs/Lending/Contract/Api.hs](https://github.com/mlabs-haskell/plutus-use-cases/blob/main/mlabs/src/Mlabs/Lending/Contract/Api.hs#L146) + +### Lendex: APIs & Endpoints + +- User Actions + + - Deposit + - [x] in use. + - Description: *Deposit funds to app.* + + - Borrow + - [x] in use. + - Description: *Borrow funds by depositing a collateral.* + + - Repay + - [x] in use. + - Description: *Repay part of a Loan.* + + - SwapBorrowRateModel + - [x] in use. + - Description: *Swap borrow interest rate strategy (stable to variable).* + + - SetUserReserveAsCollateral + - [x] in use. + - Description: *Set some portion of deposit as collateral or some portion of collateral as deposit.* + + - Withdraw + - [x] in use. + - Description: *Withdraw funds from deposit.* + + - LiquidationCall + - [x] in use. + - Description: *Call to liquidate borrows that are unsafe due to health check. For further see [docs.aave.com/faq/liquidations](https://docs.aave.com/faq/liquidations)* + +- Admin actions + + - AddReserve + - [x] in use. + - Description: *Adds a new reserve.* + + - StartParams + - [x] in use. + - Description: *Sets the start parameters for the Lendex*. + +### Lendex: Tests + +- To run the tests: + +```bash +stack test all +``` + +- To see test cases refer to: `./test/Test/Lending` + +### Lendex: Notes + +-------------------------------------------------------------------------------- + +## Use Case: NFT + +### NFT: Description + +The core functionality of the Non Fungible Tokens(i.e. NFTs) Use Case revolves +around minting, sending, receiving NFTs into a Cardano wallet. + +NFTs are a digital asset that represents real-world objects. They can be bought +and sold online, and act as a proof of ownership for the underlying asset they +are meant to represent. Fungibility is the property of an asset to be +interchangeable with its equal value in another fungible asset (example: $1 and +10x $0.10 are interchangeable). Given that real-world objects cannot be replaced +as easily with equivalent objects is a propert reflected in the nature of NFTs. + +For more details on NFT's refer to: + +- [Forbes: What You Need To Know About NFT's](https://www.forbes.com/advisor/investing/nft-non-fungible-token/) +- [Cambridge Dictionary: nonfungible](https://dictionary.cambridge.org/us/dictionary/english/nonfungible) + +### NFT: Progress & Planning + +- Goals and status: + - Development *TODO: add some achieved/ future goals* + - [x] Feature Completeness as per Specifications + - [ ] Improve Deployment Story + - [ ] Improve Performance + - [ ] Improve Ergonomics of Use and Installation + + - Testing + - [x] 100% Test Coverage + - [ ] QuickCheck Testing + + - Documentation + - [x] Example + - [ ] APIs + +### NFT: Examples + +- [NFT Demo](https://github.com/mlabs-haskell/plutus-use-cases/blob/main/mlabs/nft-demo/Main.hs) + +### NFT: APIs & Endpoints + +- User Endpoints: + - Buy + - [x] in use. + - Description: *User buys NFT.* + - SetPrice + - [x] in use. + - Description: *User sets new price for NFT.* + +- Author Endpoints: + - StartParams + - [x] in use. + - Description: *Sets the parameters to initialise a new NFT.* + +- User Schemas: + - UserSchema + - [x] in use. + - Description: *User schema. Owner can set the price and the buyer can try to buy.* + - AuthorSchema + - [x] in use. + - Description: *Schema for the author of NFT*. + +### NFT: Tests + +- To run the tests: + +```bash +stack test all +``` + +- To see test cases refer to: `./test/Test/Nft` + +### NFT: Notes + +*TODO: Add any relevant notes* + diff --git a/mlabs/Setup.hs b/mlabs/Setup.hs new file mode 100644 index 000000000..e8ef27dbb --- /dev/null +++ b/mlabs/Setup.hs @@ -0,0 +1,3 @@ +import Distribution.Simple + +main = defaultMain diff --git a/mlabs/app/Main.hs b/mlabs/app/Main.hs new file mode 100644 index 000000000..861e7ceef --- /dev/null +++ b/mlabs/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Prelude (IO, putStrLn) + +main :: IO () +main = putStrLn "Hello, Haskell!" diff --git a/mlabs/cabal.project b/mlabs/cabal.project new file mode 100644 index 000000000..a5028c431 --- /dev/null +++ b/mlabs/cabal.project @@ -0,0 +1,51 @@ +-- in-line with: 3f089ccf0ca746b399c99afe51e063b0640af547 +-- 2021/11/10 +index-state: 2021-10-20T00:00:00Z + +packages: ./. + +write-ghc-environment-files: never + +-- Always build tests and benchmarks. +tests: true +benchmarks: true + +-- The only sensible test display option +test-show-details: direct + +allow-newer: + -- Pins to an old version of Template Haskell, unclear if/when it will be updated + size-based:template-haskell + , ouroboros-consensus-byron:formatting + , beam-core:aeson + , beam-sqlite:aeson + , beam-sqlite:dlist + , beam-migrate:aeson + +constraints: + -- big breaking change here, inline-r doens't have an upper bound + singletons < 3.0 + -- bizarre issue: in earlier versions they define their own 'GEq', in newer + -- ones they reuse the one from 'some', but there isn't e.g. a proper version + -- constraint from dependent-sum-template (which is the library we actually use). + , dependent-sum > 0.6.2.0 + -- Newer Hashable have instances for Set, which breaks beam-migrate + -- which declares its own instances of Hashable Set + , hashable < 1.3.4.0 + +-- See the note on nix/pkgs/default.nix:agdaPackages for why this is here. +-- (NOTE this will change to ieee754 in newer versions of nixpkgs). +extra-packages: ieee, filemanip + +-- These packages appear in our dependency tree and are very slow to build. +-- Empirically, turning off optimization shaves off ~50% build time. +-- It also mildly improves recompilation avoidance. +-- For deve work we don't care about performance so much, so this is okay. +package cardano-ledger-alonzo + optimization: False +package ouroboros-consensus-shelley + optimization: False +package ouroboros-consensus-cardano + optimization: False +package cardano-api + optimization: False diff --git a/mlabs/data/protocol-params.json b/mlabs/data/protocol-params.json new file mode 100644 index 000000000..7bec24225 --- /dev/null +++ b/mlabs/data/protocol-params.json @@ -0,0 +1,209 @@ +{ + "txFeePerByte": 44, + "minUTxOValue": 1000000, + "stakePoolDeposit": 500000000 + "utxoCostPerWord": 34482, + "decentralization": 0, + "poolRetireMaxEpoch": 18, + "extraPraosEntropy": null, + "collateralPercentage": 150, + "stakePoolTargetNum": 100, + "maxBlockBodySize": 65536, + "maxTxSize": 16384, + "treasuryCut": 0, + "minPoolCost": 0, + "maxCollateralInputs": 3, + "maxValueSize": 5000, + "maxTxExecutionUnits": { + "memory": 14000000, + "steps": 10000000000 + }, + + "maxBlockExecutionUnits": { + "memory": 50000000, + "steps": 40000000000 + }, + "maxBlockHeaderSize": 1100, + "costModels": { + "PlutusScriptV1": { + "sha2_256-memory-arguments": 4, + "equalsString-cpu-arguments-constant": 1000, + "cekDelayCost-exBudgetMemory": 100, + "lessThanEqualsByteString-cpu-arguments-intercept": 103599, + "divideInteger-memory-arguments-minimum": 1, + "appendByteString-cpu-arguments-slope": 621, + "blake2b-cpu-arguments-slope": 29175, + "iData-cpu-arguments": 150000, + "encodeUtf8-cpu-arguments-slope": 1000, + "unBData-cpu-arguments": 150000, + "multiplyInteger-cpu-arguments-intercept": 61516, + "cekConstCost-exBudgetMemory": 100, + "nullList-cpu-arguments": 150000, + "equalsString-cpu-arguments-intercept": 150000, + "trace-cpu-arguments": 150000, + "mkNilData-memory-arguments": 32, + "lengthOfByteString-cpu-arguments": 150000, + "cekBuiltinCost-exBudgetCPU": 29773, + "bData-cpu-arguments": 150000, + "subtractInteger-cpu-arguments-slope": 0, + "unIData-cpu-arguments": 150000, + "consByteString-memory-arguments-intercept": 0, + "divideInteger-memory-arguments-slope": 1, + "divideInteger-cpu-arguments-model-arguments-slope": 118, + "listData-cpu-arguments": 150000, + "headList-cpu-arguments": 150000, + "chooseData-memory-arguments": 32, + "equalsInteger-cpu-arguments-intercept": 136542, + "sha3_256-cpu-arguments-slope": 82363, + "sliceByteString-cpu-arguments-slope": 5000, + "unMapData-cpu-arguments": 150000, + "lessThanInteger-cpu-arguments-intercept": 179690, + "mkCons-cpu-arguments": 150000, + "appendString-memory-arguments-intercept": 0, + "modInteger-cpu-arguments-model-arguments-slope": 118, + "ifThenElse-cpu-arguments": 1, + "mkNilPairData-cpu-arguments": 150000, + "lessThanEqualsInteger-cpu-arguments-intercept": 145276, + "addInteger-memory-arguments-slope": 1, + "chooseList-memory-arguments": 32, + "constrData-memory-arguments": 32, + "decodeUtf8-cpu-arguments-intercept": 150000, + "equalsData-memory-arguments": 1, + "subtractInteger-memory-arguments-slope": 1, + "appendByteString-memory-arguments-intercept": 0, + "lengthOfByteString-memory-arguments": 4, + "headList-memory-arguments": 32, + "listData-memory-arguments": 32, + "consByteString-cpu-arguments-intercept": 150000, + "unIData-memory-arguments": 32, + "remainderInteger-memory-arguments-minimum": 1, + "bData-memory-arguments": 32, + "lessThanByteString-cpu-arguments-slope": 248, + "encodeUtf8-memory-arguments-intercept": 0, + "cekStartupCost-exBudgetCPU": 100, + "multiplyInteger-memory-arguments-intercept": 0, + "unListData-memory-arguments": 32, + "remainderInteger-cpu-arguments-model-arguments-slope": 118, + "cekVarCost-exBudgetCPU": 29773, + "remainderInteger-memory-arguments-slope": 1, + "cekForceCost-exBudgetCPU": 29773, + "sha2_256-cpu-arguments-slope": 29175, + "equalsInteger-memory-arguments": 1, + "indexByteString-memory-arguments": 1, + "addInteger-memory-arguments-intercept": 1, + "chooseUnit-cpu-arguments": 150000, + "sndPair-cpu-arguments": 150000, + "cekLamCost-exBudgetCPU": 29773, + "fstPair-cpu-arguments": 150000, + "quotientInteger-memory-arguments-minimum": 1, + "decodeUtf8-cpu-arguments-slope": 1000, + "lessThanInteger-memory-arguments": 1, + "lessThanEqualsInteger-cpu-arguments-slope": 1366, + "fstPair-memory-arguments": 32, + "modInteger-memory-arguments-intercept": 0, + "unConstrData-cpu-arguments": 150000, + "lessThanEqualsInteger-memory-arguments": 1, + "chooseUnit-memory-arguments": 32, + "sndPair-memory-arguments": 32, + "addInteger-cpu-arguments-intercept": 197209, + "decodeUtf8-memory-arguments-slope": 8, + "equalsData-cpu-arguments-intercept": 150000, + "mapData-cpu-arguments": 150000, + "mkPairData-cpu-arguments": 150000, + "quotientInteger-cpu-arguments-constant": 148000, + "consByteString-memory-arguments-slope": 1, + "cekVarCost-exBudgetMemory": 100, + "indexByteString-cpu-arguments": 150000, + "unListData-cpu-arguments": 150000, + "equalsInteger-cpu-arguments-slope": 1326, + "cekStartupCost-exBudgetMemory": 100, + "subtractInteger-cpu-arguments-intercept": 197209, + "divideInteger-cpu-arguments-model-arguments-intercept": 425507, + "divideInteger-memory-arguments-intercept": 0, + "cekForceCost-exBudgetMemory": 100, + "blake2b-cpu-arguments-intercept": 2477736, + "remainderInteger-cpu-arguments-constant": 148000, + "tailList-cpu-arguments": 150000, + "encodeUtf8-cpu-arguments-intercept": 150000, + "equalsString-cpu-arguments-slope": 1000, + "lessThanByteString-memory-arguments": 1, + "multiplyInteger-cpu-arguments-slope": 11218, + "appendByteString-cpu-arguments-intercept": 396231, + "lessThanEqualsByteString-cpu-arguments-slope": 248, + "modInteger-memory-arguments-slope": 1, + "addInteger-cpu-arguments-slope": 0, + "equalsData-cpu-arguments-slope": 10000, + "decodeUtf8-memory-arguments-intercept": 0, + "chooseList-cpu-arguments": 150000, + "constrData-cpu-arguments": 150000, + "equalsByteString-memory-arguments": 1, + "cekApplyCost-exBudgetCPU": 29773, + "quotientInteger-memory-arguments-slope": 1, + "verifySignature-cpu-arguments-intercept": 3345831, + "unMapData-memory-arguments": 32, + "mkCons-memory-arguments": 32, + "sliceByteString-memory-arguments-slope": 1, + "sha3_256-memory-arguments": 4, + "ifThenElse-memory-arguments": 1, + "mkNilPairData-memory-arguments": 32, + "equalsByteString-cpu-arguments-slope": 247, + "appendString-cpu-arguments-intercept": 150000, + "quotientInteger-cpu-arguments-model-arguments-slope": 118, + "cekApplyCost-exBudgetMemory": 100, + "equalsString-memory-arguments": 1, + "multiplyInteger-memory-arguments-slope": 1, + "cekBuiltinCost-exBudgetMemory": 100, + "remainderInteger-memory-arguments-intercept": 0, + "sha2_256-cpu-arguments-intercept": 2477736, + "remainderInteger-cpu-arguments-model-arguments-intercept": 425507, + "lessThanEqualsByteString-memory-arguments": 1, + "tailList-memory-arguments": 32, + "mkNilData-cpu-arguments": 150000, + "chooseData-cpu-arguments": 150000, + "unBData-memory-arguments": 32, + "blake2b-memory-arguments": 4, + "iData-memory-arguments": 32, + "nullList-memory-arguments": 32, + "cekDelayCost-exBudgetCPU": 29773, + "subtractInteger-memory-arguments-intercept": 1, + "lessThanByteString-cpu-arguments-intercept": 103599, + "consByteString-cpu-arguments-slope": 1000, + "appendByteString-memory-arguments-slope": 1, + "trace-memory-arguments": 32, + "divideInteger-cpu-arguments-constant": 148000, + "cekConstCost-exBudgetCPU": 29773, + "encodeUtf8-memory-arguments-slope": 8, + "quotientInteger-cpu-arguments-model-arguments-intercept": 425507, + "mapData-memory-arguments": 32, + "appendString-cpu-arguments-slope": 1000, + "modInteger-cpu-arguments-constant": 148000, + "verifySignature-cpu-arguments-slope": 1, + "unConstrData-memory-arguments": 32, + "quotientInteger-memory-arguments-intercept": 0, + "equalsByteString-cpu-arguments-constant": 150000, + "sliceByteString-memory-arguments-intercept": 0, + "mkPairData-memory-arguments": 32, + "equalsByteString-cpu-arguments-intercept": 112536, + "appendString-memory-arguments-slope": 1, + "lessThanInteger-cpu-arguments-slope": 497, + "modInteger-cpu-arguments-model-arguments-intercept": 425507, + "modInteger-memory-arguments-minimum": 1, + "sha3_256-cpu-arguments-intercept": 0, + "verifySignature-memory-arguments": 1, + "cekLamCost-exBudgetMemory": 100, + "sliceByteString-cpu-arguments-intercept": 150000 + } + }, + "protocolVersion": { + "minor": 0, + "major": 6 + }, + "txFeeFixed": 155381, + "stakeAddressDeposit": 0, + "monetaryExpansion": 0, + "poolPledgeInfluence": 0, + "executionUnitPrices": { + "priceSteps": 7.21e-5, + "priceMemory": 5.77e-2 + } +} diff --git a/mlabs/default.nix b/mlabs/default.nix new file mode 100644 index 000000000..9ffc54a83 --- /dev/null +++ b/mlabs/default.nix @@ -0,0 +1,13 @@ +( + import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) { + src = ./.; + } +).defaultNix diff --git a/mlabs/demo-frontend/package-lock.json b/mlabs/demo-frontend/package-lock.json new file mode 100644 index 000000000..52add8998 --- /dev/null +++ b/mlabs/demo-frontend/package-lock.json @@ -0,0 +1,9494 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/compat-data": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.7.tgz", + "integrity": "sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw==" + }, + "@babel/core": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.3.4.tgz", + "integrity": "sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.3.4", + "@babel/helpers": "^7.2.0", + "@babel/parser": "^7.3.4", + "@babel/template": "^7.2.2", + "@babel/traverse": "^7.3.4", + "@babel/types": "^7.3.4", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.11", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/generator": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.4.tgz", + "integrity": "sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==", + "requires": { + "@babel/types": "^7.3.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz", + "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz", + "integrity": "sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w==", + "requires": { + "@babel/helper-explode-assignable-expression": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-builder-react-jsx": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.14.5.tgz", + "integrity": "sha512-LT/856RUBXAHjmvJuLuI6XYZZAZNMSS+N2Yf5EUoHgSWtiWrAaGh7t5saP7sPCq07uvWVxxK3gwwm3weA9gKLg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-compilation-targets": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz", + "integrity": "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==", + "requires": { + "@babel/compat-data": "^7.14.5", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", + "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "regexpu-core": "^4.7.1" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz", + "integrity": "sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", + "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/parser": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==" + }, + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-get-function-arity": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", + "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-hoist-variables": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", + "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", + "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-module-imports": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", + "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-module-transforms": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz", + "integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==", + "requires": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5", + "@babel/helper-simple-access": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", + "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", + "requires": { + "@babel/types": "^7.14.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/parser": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==" + }, + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/traverse": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/parser": "^7.14.7", + "@babel/types": "^7.14.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", + "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==" + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.14.5.tgz", + "integrity": "sha512-rLQKdQU+HYlxBwQIj8dk4/0ENOUEhA/Z0l4hN8BexpvmSMN9oA9EagjnhnDpNsRdWCfjwa4mn/HyBXO9yhQP6A==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-wrap-function": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-replace-supers": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", + "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.14.5", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", + "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", + "requires": { + "@babel/types": "^7.14.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/parser": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==" + }, + "@babel/traverse": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/parser": "^7.14.7", + "@babel/types": "^7.14.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/helper-simple-access": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz", + "integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz", + "integrity": "sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", + "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==" + }, + "@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==" + }, + "@babel/helper-wrap-function": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.14.5.tgz", + "integrity": "sha512-YEdjTCq+LNuNS1WfxsDCNpgXkJaIyqco6DAelTUjT4f2KIWC1nBcaCaSdHTBqQVLnTBexBcVcFhLSU1KnYuePQ==", + "requires": { + "@babel/helper-function-name": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", + "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", + "requires": { + "@babel/types": "^7.14.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/parser": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==" + }, + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/traverse": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/parser": "^7.14.7", + "@babel/types": "^7.14.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/helpers": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.6.tgz", + "integrity": "sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA==", + "requires": { + "@babel/template": "^7.14.5", + "@babel/traverse": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", + "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", + "requires": { + "@babel/types": "^7.14.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/parser": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==" + }, + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/traverse": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/parser": "^7.14.7", + "@babel/types": "^7.14.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.4.tgz", + "integrity": "sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==" + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.7.tgz", + "integrity": "sha512-RK8Wj7lXLY3bqei69/cc25gwS5puEc3dknoFPFbqfy3XxYQBQFvu4ioWpafMBAB+L9NyptQK4nMOa5Xz16og8Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.14.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz", + "integrity": "sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz", + "integrity": "sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g==", + "requires": { + "@babel/compat-data": "^7.14.7", + "@babel/helper-compilation-targets": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.14.5" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz", + "integrity": "sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz", + "integrity": "sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-flow": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.14.5.tgz", + "integrity": "sha512-9WK5ZwKCdWHxVuU13XNT6X73FGmutAXeor5lGFq6qhOFtMFUF4jkbijuyUdZZlpYq6E2hZeZf/u3959X9wsv0Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", + "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz", + "integrity": "sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz", + "integrity": "sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA==", + "requires": { + "@babel/helper-module-imports": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.14.5" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz", + "integrity": "sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.5.tgz", + "integrity": "sha512-LBYm4ZocNgoCqyxMLoOnwpsmQ18HWTQvql64t3GvMUzLQrNoV1BDG0lNftC8QKYERkZgCCT/7J5xWGObGAyHDw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.5.tgz", + "integrity": "sha512-J4VxKAMykM06K/64z9rwiL6xnBHgB1+FVspqvlgCdwD1KUbQNfszeKVVOMh59w3sztHYIZDgnhOC4WbdEfHFDA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.14.5", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-optimise-call-expression": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz", + "integrity": "sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz", + "integrity": "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz", + "integrity": "sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz", + "integrity": "sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz", + "integrity": "sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA==", + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-flow-strip-types": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.3.4.tgz", + "integrity": "sha512-PmQC9R7DwpBFA+7ATKMyzViz3zCaMNouzZMPZN2K5PnbBbtL3AXFYTkDk+Hey5crQq2A90UG5Uthz0mel+XZrA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.2.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz", + "integrity": "sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz", + "integrity": "sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ==", + "requires": { + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz", + "integrity": "sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz", + "integrity": "sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g==", + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz", + "integrity": "sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ==", + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz", + "integrity": "sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA==", + "requires": { + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz", + "integrity": "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==", + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.7.tgz", + "integrity": "sha512-DTNOTaS7TkW97xsDMrp7nycUVh6sn/eq22VaxWfEdzuEbRsiaOU0pqU7DlyUGHVsbQbSghvjKRpEl+nUCKGQSg==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz", + "integrity": "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz", + "integrity": "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz", + "integrity": "sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz", + "integrity": "sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==", + "requires": { + "@babel/helper-builder-react-jsx": "^7.3.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz", + "integrity": "sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg==", + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz", + "integrity": "sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz", + "integrity": "sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz", + "integrity": "sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz", + "integrity": "sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz", + "integrity": "sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz", + "integrity": "sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/preset-env": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.3.4.tgz", + "integrity": "sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.3.4", + "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.2.0", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-async-to-generator": "^7.3.4", + "@babel/plugin-transform-block-scoped-functions": "^7.2.0", + "@babel/plugin-transform-block-scoping": "^7.3.4", + "@babel/plugin-transform-classes": "^7.3.4", + "@babel/plugin-transform-computed-properties": "^7.2.0", + "@babel/plugin-transform-destructuring": "^7.2.0", + "@babel/plugin-transform-dotall-regex": "^7.2.0", + "@babel/plugin-transform-duplicate-keys": "^7.2.0", + "@babel/plugin-transform-exponentiation-operator": "^7.2.0", + "@babel/plugin-transform-for-of": "^7.2.0", + "@babel/plugin-transform-function-name": "^7.2.0", + "@babel/plugin-transform-literals": "^7.2.0", + "@babel/plugin-transform-modules-amd": "^7.2.0", + "@babel/plugin-transform-modules-commonjs": "^7.2.0", + "@babel/plugin-transform-modules-systemjs": "^7.3.4", + "@babel/plugin-transform-modules-umd": "^7.2.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.3.0", + "@babel/plugin-transform-new-target": "^7.0.0", + "@babel/plugin-transform-object-super": "^7.2.0", + "@babel/plugin-transform-parameters": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.3.4", + "@babel/plugin-transform-shorthand-properties": "^7.2.0", + "@babel/plugin-transform-spread": "^7.2.0", + "@babel/plugin-transform-sticky-regex": "^7.2.0", + "@babel/plugin-transform-template-literals": "^7.2.0", + "@babel/plugin-transform-typeof-symbol": "^7.2.0", + "@babel/plugin-transform-unicode-regex": "^7.2.0", + "browserslist": "^4.3.4", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.3.0" + } + }, + "@babel/runtime": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz", + "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==", + "requires": { + "regenerator-runtime": "^0.12.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" + } + } + }, + "@babel/template": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", + "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.2.2", + "@babel/types": "^7.2.2" + } + }, + "@babel/traverse": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.3.4.tgz", + "integrity": "sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.3.4", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/parser": "^7.3.4", + "@babel/types": "^7.3.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.11" + } + }, + "@babel/types": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz", + "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==", + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + }, + "@discoveryjs/json-ext": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", + "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==" + }, + "@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" + }, + "@parcel/fs": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-1.11.0.tgz", + "integrity": "sha512-86RyEqULbbVoeo8OLcv+LQ1Vq2PKBAvWTU9fCgALxuCTbbs5Ppcvll4Vr+Ko1AnmMzja/k++SzNAwJfeQXVlpA==", + "requires": { + "@parcel/utils": "^1.11.0", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.2" + } + }, + "@parcel/logger": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-1.11.1.tgz", + "integrity": "sha512-9NF3M6UVeP2udOBDILuoEHd8VrF4vQqoWHEafymO1pfSoOMfxrSJZw1MfyAAIUN/IFp9qjcpDCUbDZB+ioVevA==", + "requires": { + "@parcel/workers": "^1.11.0", + "chalk": "^2.1.0", + "grapheme-breaker": "^0.3.2", + "ora": "^2.1.0", + "strip-ansi": "^4.0.0" + } + }, + "@parcel/utils": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-1.11.0.tgz", + "integrity": "sha512-cA3p4jTlaMeOtAKR/6AadanOPvKeg8VwgnHhOyfi0yClD0TZS/hi9xu12w4EzA/8NtHu0g6o4RDfcNjqN8l1AQ==" + }, + "@parcel/watcher": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-1.12.1.tgz", + "integrity": "sha512-od+uCtCxC/KoNQAIE1vWx1YTyKYY+7CTrxBJPRh3cDWw/C0tCtlBMVlrbplscGoEpt6B27KhJDCv82PBxOERNA==", + "requires": { + "@parcel/utils": "^1.11.0", + "chokidar": "^2.1.5" + } + }, + "@parcel/workers": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-1.11.0.tgz", + "integrity": "sha512-USSjRAAQYsZFlv43FUPdD+jEGML5/8oLF0rUzPQTtK4q9kvaXr49F5ZplyLz5lox78cLZ0TxN2bIDQ1xhOkulQ==", + "requires": { + "@parcel/utils": "^1.11.0", + "physical-cpu-count": "^2.0.0" + } + }, + "@types/eslint": { + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.13.tgz", + "integrity": "sha512-LKmQCWAlnVHvvXq4oasNUMTJJb2GwSyTY8+1C7OH5ILR8mPLaljv1jxL1bXW3xB3jFbQxTKxJAvI8PyjB09aBg==", + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz", + "integrity": "sha512-O/ql2+rrCUe2W2rs7wMR+GqPRcgB6UiqN5RhrR5xruFlY7l9YLMn0ZkDzjoHLeiFkR8MCQZVudUuuvQ2BLC9Qw==", + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.47", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.47.tgz", + "integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==" + }, + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==" + }, + "@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" + }, + "@types/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==" + }, + "@types/node": { + "version": "15.12.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.5.tgz", + "integrity": "sha512-se3yX7UHv5Bscf8f1ERKvQOD6sTyycH3hdaoozvaLxgUiY5lIGEeH37AD0G0Qi9kPqihPn0HOfd2yaIEN9VwEg==" + }, + "@types/q": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", + "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==" + }, + "@webassemblyjs/ast": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.0.tgz", + "integrity": "sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg==", + "requires": { + "@webassemblyjs/helper-numbers": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz", + "integrity": "sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA==" + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz", + "integrity": "sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w==" + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz", + "integrity": "sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA==" + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz", + "integrity": "sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ==", + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz", + "integrity": "sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA==" + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz", + "integrity": "sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz", + "integrity": "sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA==", + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.0.tgz", + "integrity": "sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g==", + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.0.tgz", + "integrity": "sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw==" + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz", + "integrity": "sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/helper-wasm-section": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-opt": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "@webassemblyjs/wast-printer": "1.11.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz", + "integrity": "sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz", + "integrity": "sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-buffer": "1.11.0", + "@webassemblyjs/wasm-gen": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz", + "integrity": "sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/helper-api-error": "1.11.0", + "@webassemblyjs/helper-wasm-bytecode": "1.11.0", + "@webassemblyjs/ieee754": "1.11.0", + "@webassemblyjs/leb128": "1.11.0", + "@webassemblyjs/utf8": "1.11.0" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz", + "integrity": "sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ==", + "requires": { + "@webassemblyjs/ast": "1.11.0", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.0.4.tgz", + "integrity": "sha512-cs3XLy+UcxiP6bj0A6u7MLLuwdXJ1c3Dtc0RkKg+wiI1g/Ti1om8+/2hc2A2B60NbBNAbMgyBMHvyymWm/j4wQ==" + }, + "@webpack-cli/info": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.3.0.tgz", + "integrity": "sha512-ASiVB3t9LOKHs5DyVUcxpraBXDOKubYu/ihHhU+t1UPpxsivg6Od2E2qU4gJCekfEddzRBzHhzA/Acyw/mlK/w==", + "requires": { + "envinfo": "^7.7.3" + } + }, + "@webpack-cli/serve": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.5.1.tgz", + "integrity": "sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw==" + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" + } + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==" + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=" + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansi-to-html": { + "version": "0.6.15", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.6.15.tgz", + "integrity": "sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==", + "requires": { + "entities": "^2.0.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + }, + "dependencies": { + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + } + } + }, + "babylon-walk": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/babylon-walk/-/babylon-walk-1.0.2.tgz", + "integrity": "sha1-OxWl3btIKni0zpwByLoYFwLZ1s4=", + "requires": { + "babel-runtime": "^6.11.6", + "babel-types": "^6.15.0", + "lodash.clone": "^4.5.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brfs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", + "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==", + "requires": { + "quote-stream": "^1.0.1", + "resolve": "^1.1.5", + "static-module": "^2.2.0", + "through2": "^2.0.0" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "requires": { + "pako": "~1.0.5" + }, + "dependencies": { + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + } + } + }, + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "requires": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cacache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz", + "integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==", + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001240", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001240.tgz", + "integrity": "sha512-nb8mDzfMdxBDN7ZKx8chWafAdBp5DAAlpWvNyUGe5tcDWd838zpzDN3Rah9cjCqhfOKkrvx40G2SDtP0qiWX/w==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "requires": { + "source-map": "~0.6.0" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-spinners": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", + "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==" + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "clones": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/clones/-/clones-1.2.0.tgz", + "integrity": "sha512-FXDYw4TjR8wgPZYui2LeTqWh1BLpfQ8lB6upMtlpDF6WlOOxghmTTxWyngdKTgozqBgKnHbTVwTE+hOHqAykuQ==" + }, + "coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "requires": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", + "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.4" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", + "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==" + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==" + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors-anywhere": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cors-anywhere/-/cors-anywhere-0.4.4.tgz", + "integrity": "sha512-8OBFwnzMgR4mNrAeAyOLB2EruS2z7u02of2bOu7i9kKYlZG+niS7CTHLPgEXKWW2NAOJWRry9RRCaL9lJRjNqg==", + "requires": { + "http-proxy": "1.11.1", + "proxy-from-env": "0.0.1" + } + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=" + }, + "css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "requires": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + } + }, + "css-modules-loader-core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", + "integrity": "sha1-WQhmgpShvs0mGuCkziGwtVHyHRY=", + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.1", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "postcss": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.1.tgz", + "integrity": "sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=", + "requires": { + "chalk": "^1.1.3", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" + }, + "css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "requires": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "requires": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "cssnano": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz", + "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==", + "requires": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.8", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "cssnano-preset-default": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz", + "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==", + "requires": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.3", + "postcss-unique-selectors": "^4.0.1" + } + }, + "cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=" + }, + "cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=" + }, + "cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "requires": { + "postcss": "^7.0.0" + } + }, + "cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==" + }, + "csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "requires": { + "css-tree": "^1.1.2" + }, + "dependencies": { + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + } + } + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + }, + "cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "requires": { + "cssom": "0.3.x" + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" + }, + "dargs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-5.1.0.tgz", + "integrity": "sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "deasync": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.21.tgz", + "integrity": "sha512-kUmM8Y+PZpMpQ+B4AuOW9k2Pfx/mSupJtxOsLzmnHY2WqZUYRFccFn2RhzPAqt3Xb+sorK/badW2D4zNzqZz5w==", + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^1.7.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "requires": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + } + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + }, + "dependencies": { + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + } + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" + }, + "dns-packet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", + "requires": { + "domelementtype": "^2.2.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + } + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "dotenv": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", + "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" + }, + "dotenv-expand": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-4.2.0.tgz", + "integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=" + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "requires": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "electron-to-chromium": { + "version": "1.3.759", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.759.tgz", + "integrity": "sha512-nM76xH0t2FBH5iMEZDVc3S/qbdKjGH7TThezxC8k1Q7w7WHvIAyJh8lAe2UamGfdRqBTjHfPDn82LJ0ksCiB9g==" + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz", + "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + }, + "envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==" + }, + "err-code": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "dependencies": { + "object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" + } + } + }, + "es-module-lexer": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.6.0.tgz", + "integrity": "sha512-f8kcHX1ArhllUtb/wVSyvygoKCznIjnxhLxy7TCvIiMdT7fL4ZDTIKaadMe6eLvOXg6Wk02UeoFgUoZ2EKZZUA==" + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", + "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "eventemitter3": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", + "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "eventsource": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", + "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "requires": { + "original": "^1.0.0" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", + "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^3.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "falafel": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.4.tgz", + "integrity": "sha512-0HXjo8XASWRmsS0X1EkhwEMZaD3Qvp7FfURwjLKjG1ghfRm/MGZl2r4cWUTv41KdNghTw4OUMmVtdGQp3+H+uQ==", + "requires": { + "acorn": "^7.1.1", + "foreach": "^2.0.5", + "isarray": "^2.0.1", + "object-keys": "^1.0.6" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + } + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==" + }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==" + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "requires": { + "minipass": "^2.6.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=" + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "grapheme-breaker": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/grapheme-breaker/-/grapheme-breaker-0.3.2.tgz", + "integrity": "sha1-W55reMODJFLSuiuxy4MPlidkEKw=", + "requires": { + "brfs": "^1.2.0", + "unicode-trie": "^0.3.1" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + } + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=" + }, + "hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=" + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==" + }, + "html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "requires": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + } + } + }, + "html-tags": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-1.2.0.tgz", + "integrity": "sha1-x43mW1Zjqll5id0rerSSANfk25g=" + }, + "html-webpack-plugin": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.3.2.tgz", + "integrity": "sha512-HvB33boVNCz2lTyBsSiMffsJ+m0YLIQ+pskblXgN9fnjS1BgEcuAfdInfXfGrkdXV406k9FiDi86eVCDBgJOyQ==", + "requires": { + "@types/html-minifier-terser": "^5.0.0", + "html-minifier-terser": "^5.0.1", + "lodash": "^4.17.21", + "pretty-error": "^3.0.4", + "tapable": "^2.0.0" + } + }, + "htmlnano": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-0.2.9.tgz", + "integrity": "sha512-jWTtP3dCd7R8x/tt9DK3pvpcQd7HDMcRPUqPxr/i9989q2k5RHIhmlRDFeyQ/LSd8IKrteG8Ce5g0Ig4eGIipg==", + "requires": { + "cssnano": "^4.1.11", + "posthtml": "^0.15.1", + "purgecss": "^2.3.0", + "relateurl": "^0.2.7", + "srcset": "^3.0.0", + "svgo": "^1.3.2", + "terser": "^5.6.1", + "timsort": "^0.3.0", + "uncss": "^0.17.3" + }, + "dependencies": { + "posthtml": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.15.2.tgz", + "integrity": "sha512-YugEJ5ze/0DLRIVBjCpDwANWL4pPj1kHJ/2llY8xuInr0nbkon3qTiMPe5LQa+cCwNjxS7nAZZTp+1M+6mT4Zg==", + "requires": { + "posthtml-parser": "^0.7.2", + "posthtml-render": "^1.3.1" + } + }, + "posthtml-parser": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.7.2.tgz", + "integrity": "sha512-LjEEG/3fNcWZtBfsOE3Gbyg1Li4CmsZRkH1UmbMR7nKdMXVMYI3B4/ZMiCpaq8aI1Aym4FRMMW9SAOLSwOnNsQ==", + "requires": { + "htmlparser2": "^6.0.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + }, + "terser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz", + "integrity": "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + } + } + } + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==" + }, + "http-proxy": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.11.1.tgz", + "integrity": "sha1-cd9VdX6ALVjqgQ3yJEAZ3aBa6F0=", + "requires": { + "eventemitter3": "1.x.x", + "requires-port": "0.x.x" + } + }, + "http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "requires": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "requires": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + } + }, + "interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arguments": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-bigint": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", + "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", + "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + }, + "is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "requires": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "requires": { + "has": "^1.0.3" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", + "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==" + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-html": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-html/-/is-html-1.1.0.tgz", + "integrity": "sha1-4E8cGNOUhRETlvmgJz6rUa8hhGQ=", + "requires": { + "html-tags": "^1.0.0" + } + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-number-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", + "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==" + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "is-string": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", + "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==" + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jest-worker": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.2.tgz", + "integrity": "sha512-EoBdilOTTyOgmHXtw/cPc+ZrCA0KJMrkXzkrPGNwLmnvvlN1nj7MPrxpT7m+otSv2e1TLaVffzDnE/LB14zJMg==", + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-beautify": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.0.tgz", + "integrity": "sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ==", + "requires": { + "config-chain": "^1.1.12", + "editorconfig": "^0.15.3", + "glob": "^7.1.3", + "nopt": "^5.0.0" + } + }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==" + }, + "js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jsdom": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-14.1.0.tgz", + "integrity": "sha512-O901mfJSuTdwU2w3Sn+74T+RnDVP+FuV5fH8tcPWyqrseRAb0s5xOtPgCFiPOtLcyK7CLIJwPyD83ZqQWvA5ng==", + "requires": { + "abab": "^2.0.0", + "acorn": "^6.0.4", + "acorn-globals": "^4.3.0", + "array-equal": "^1.0.0", + "cssom": "^0.3.4", + "cssstyle": "^1.1.1", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.0", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.1.3", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.5", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.5.0", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^6.1.2", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==" + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "requires": { + "minimist": "^1.2.0" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==" + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==" + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=" + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "^2.0.1" + } + }, + "log-update": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-3.4.0.tgz", + "integrity": "sha512-ILKe88NeMt4gmDvk/eb615U/IVn7K9KWGkoYbdatQ69Z65nj1ZzjM6fHXfcs0Uge+e+EGnMW7DY4T9yko8vWFg==", + "requires": { + "ansi-escapes": "^3.2.0", + "cli-cursor": "^2.1.0", + "wrap-ansi": "^5.0.0" + } + }, + "loglevel": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "requires": { + "tslib": "^2.0.3" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "magic-string": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", + "requires": { + "vlq": "^0.2.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "merge-source-map": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", + "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=", + "requires": { + "source-map": "^0.5.6" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", + "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" + }, + "mime-types": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", + "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "requires": { + "mime-db": "1.48.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "requires": { + "minipass": "^2.9.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" + }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==" + }, + "node-forge": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "node-releases": { + "version": "1.1.73", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", + "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==" + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==" + }, + "npm-run-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", + "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", + "requires": { + "path-key": "^3.0.0" + }, + "dependencies": { + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + } + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", + "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==" + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", + "integrity": "sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", + "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.2" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "ora": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-2.1.0.tgz", + "integrity": "sha512-hNNlAd3gfv/iPmsNxYoAPLvxg7HuPozww7fFonMZvL84tP6Ox5igfk5j/+a9rtJJwqMgKK+JgWsAQik5o0HTLA==", + "requires": { + "chalk": "^2.3.1", + "cli-cursor": "^2.1.0", + "cli-spinners": "^1.1.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^4.0.0", + "wcwidth": "^1.0.1" + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==" + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" + }, + "p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "requires": { + "retry": "^0.12.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "parcel": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/parcel/-/parcel-1.12.3.tgz", + "integrity": "sha512-j9XCVLeol9qZvGemRKt2z8bptbXq9LVy8/IzjqWQKMiKd8DR0NpDAlRHV0zyF72/J/UUTsdsrhnw6UGo9nGI+Q==", + "requires": { + "@babel/code-frame": "^7.0.0 <7.4.0", + "@babel/core": "^7.0.0 <7.4.0", + "@babel/generator": "^7.0.0 <7.4.0", + "@babel/parser": "^7.0.0 <7.4.0", + "@babel/plugin-transform-flow-strip-types": "^7.0.0 <7.4.0", + "@babel/plugin-transform-modules-commonjs": "^7.0.0 <7.4.0", + "@babel/plugin-transform-react-jsx": "^7.0.0 <7.4.0", + "@babel/preset-env": "^7.0.0 <7.4.0", + "@babel/runtime": "^7.0.0 <7.4.0", + "@babel/template": "^7.0.0 <7.4.0", + "@babel/traverse": "^7.0.0 <7.4.0", + "@babel/types": "^7.0.0 <7.4.0", + "@iarna/toml": "^2.2.0", + "@parcel/fs": "^1.11.0", + "@parcel/logger": "^1.11.0", + "@parcel/utils": "^1.11.0", + "@parcel/watcher": "^1.12.0", + "@parcel/workers": "^1.11.0", + "ansi-to-html": "^0.6.4", + "babylon-walk": "^1.0.2", + "browserslist": "^4.1.0", + "chalk": "^2.1.0", + "clone": "^2.1.1", + "command-exists": "^1.2.6", + "commander": "^2.11.0", + "cross-spawn": "^6.0.4", + "css-modules-loader-core": "^1.1.0", + "cssnano": "^4.0.0", + "deasync": "^0.1.14", + "dotenv": "^5.0.0", + "dotenv-expand": "^4.2.0", + "fast-glob": "^2.2.2", + "filesize": "^3.6.0", + "get-port": "^3.2.0", + "htmlnano": "^0.2.2", + "is-glob": "^4.0.0", + "is-url": "^1.2.2", + "js-yaml": "^3.10.0", + "json5": "^1.0.1", + "micromatch": "^3.0.4", + "mkdirp": "^0.5.1", + "node-forge": "^0.7.1", + "node-libs-browser": "^2.0.0", + "opn": "^5.1.0", + "postcss": "^7.0.11", + "postcss-value-parser": "^3.3.1", + "posthtml": "^0.11.2", + "posthtml-parser": "^0.4.0", + "posthtml-render": "^1.1.3", + "resolve": "^1.4.0", + "semver": "^5.4.1", + "serialize-to-js": "^1.1.1", + "serve-static": "^1.12.4", + "source-map": "0.6.1", + "terser": "^3.7.3", + "v8-compile-cache": "^2.0.0", + "ws": "^5.1.1" + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "physical-cpu-count": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/physical-cpu-count/-/physical-cpu-count-2.0.0.tgz", + "integrity": "sha1-GN4vl+S/epVRrXURlCtUlverpmA=" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "postcss": { + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-calc": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", + "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", + "requires": { + "postcss": "^7.0.27", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + } + } + }, + "postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "requires": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "requires": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + } + }, + "postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "requires": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "requires": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "requires": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "requires": { + "postcss": "^7.0.0" + } + }, + "postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "requires": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "requires": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "requires": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "requires": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "requires": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "requires": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz", + "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==", + "requires": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + } + }, + "postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "requires": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + }, + "posthtml": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.11.6.tgz", + "integrity": "sha512-C2hrAPzmRdpuL3iH0TDdQ6XCc9M7Dcc3zEW5BLerY65G4tWWszwv6nG/ksi6ul5i2mx22ubdljgktXCtNkydkw==", + "requires": { + "posthtml-parser": "^0.4.1", + "posthtml-render": "^1.1.5" + } + }, + "posthtml-parser": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.4.2.tgz", + "integrity": "sha512-BUIorsYJTvS9UhXxPTzupIztOMVNPa/HtAm9KHni9z6qEfiJ1bpOBL5DfUOL9XAc3XkLIEzBzpph+Zbm4AdRAg==", + "requires": { + "htmlparser2": "^3.9.2" + }, + "dependencies": { + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "posthtml-render": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-1.4.0.tgz", + "integrity": "sha512-W1779iVHGfq0Fvh2PROhCe2QhB8mEErgqzo1wpIt36tCgChafP+hbXIhLDOM8ePJrZcFs0vkNEtdibEWVqChqw==" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "pretty-error": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-3.0.4.tgz", + "integrity": "sha512-ytLFLfv1So4AO1UkoBF6GXQgJRaKbiSiGFICaOPNwQ3CMvBvXpLRubeQWyPGnsbV/t9ml9qto6IeCsho0aEvwQ==", + "requires": { + "lodash": "^4.17.20", + "renderkid": "^2.0.6" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "promise-retry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + }, + "dependencies": { + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + } + } + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxy-from-env": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-0.0.1.tgz", + "integrity": "sha1-snxJRunm1dutt1mKZDXTAUxM/Uk=" + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "purescript": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/purescript/-/purescript-0.14.2.tgz", + "integrity": "sha512-kEXY5yUaG8a1FNN/IdtfNl4gcql7p76CPqnanMZ37GdtBZTcFK/SB24bp2rOAT1/N9qU8/corlra6uNf4+5pgQ==", + "requires": { + "purescript-installer": "^0.2.0" + } + }, + "purescript-installer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/purescript-installer/-/purescript-installer-0.2.5.tgz", + "integrity": "sha512-fQAWWP5a7scuchXecjpU4r4KEgSPuS6bBnaP01k9f71qqD28HaJ2m4PXHFkhkR4oATAxTPIGCtmTwtVoiBOHog==", + "requires": { + "arch": "^2.1.1", + "byline": "^5.0.0", + "cacache": "^11.3.2", + "chalk": "^2.4.2", + "env-paths": "^2.2.0", + "execa": "^2.0.3", + "filesize": "^4.1.2", + "is-plain-obj": "^2.0.0", + "log-symbols": "^3.0.0", + "log-update": "^3.2.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "ms": "^2.1.2", + "once": "^1.4.0", + "pump": "^3.0.0", + "request": "^2.88.0", + "rimraf": "^2.6.3", + "tar": "^4.4.6", + "which": "^1.3.1", + "zen-observable": "^0.8.14" + }, + "dependencies": { + "filesize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-4.2.1.tgz", + "integrity": "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA==" + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "requires": { + "chalk": "^2.4.2" + } + } + } + }, + "purescript-psa": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/purescript-psa/-/purescript-psa-0.8.2.tgz", + "integrity": "sha512-4Olf0aQQrNCfcDLXQI3gJgINEQ+3U+4QPLmQ2LHX2L/YOXSwM7fOGIUs/wMm/FQnwERUyQmHKQTJKB4LIjE2fg==" + }, + "purgecss": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-2.3.0.tgz", + "integrity": "sha512-BE5CROfVGsx2XIhxGuZAT7rTH9lLeQx/6M0P7DTXQH4IUc3BBzs9JUzt4yzGf3JrH9enkeq6YJBe9CTtkm1WmQ==", + "requires": { + "commander": "^5.0.0", + "glob": "^7.0.0", + "postcss": "7.0.32", + "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + }, + "postcss": { + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", + "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "purs-loader": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/purs-loader/-/purs-loader-3.7.2.tgz", + "integrity": "sha512-Sidqk2RE1R2DTPt30I6G3p//c9pMaV9jd36NI3HXXSyf4Kf5X01FiP/2wMTJ8a5XKAXKdKCJ3WPqA8Whlxi0tg==", + "requires": { + "bluebird": "^3.3.5", + "chalk": "^1.1.3", + "cross-spawn": "^3.0.1", + "dargs": "^5.1.0", + "debug": "^2.6.0", + "globby": "^4.0.0", + "js-string-escape": "^1.0.1", + "loader-utils": "^1.0.2", + "lodash.difference": "^4.5.0", + "promise-retry": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globby": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-4.1.0.tgz", + "integrity": "sha1-CA9UVJ7BuCpsYOYx/ILhIR2+lfg=", + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^6.0.1", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "quote-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", + "integrity": "sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=", + "requires": { + "buffer-equal": "0.0.1", + "minimist": "^1.1.3", + "through2": "^2.0.0" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "rechoir": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz", + "integrity": "sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==", + "requires": { + "resolve": "^1.9.0" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "regenerate-unicode-properties": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", + "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, + "regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "requires": { + "@babel/runtime": "^7.8.4" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", + "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "regexpu-core": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" + } + }, + "regjsgen": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==" + }, + "regjsparser": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.9.tgz", + "integrity": "sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==", + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "renderkid": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "requires": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + } + }, + "css-what": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==" + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "nth-check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", + "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "requires": { + "boolbase": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "requires-port": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-0.0.1.tgz", + "integrity": "sha1-S0QUQR2d98hVmV3YmajHiilRwW0=" + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + } + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, + "rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=" + }, + "rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "requires": { + "aproba": "^1.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "safer-eval": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/safer-eval/-/safer-eval-1.3.6.tgz", + "integrity": "sha512-DN9tBsZgtUOHODzSfO1nGCLhZtxc7Qq/d8/2SNxQZ9muYXZspSh1fO7HOsrf4lcelBNviAJLCxB/ggmG+jV1aw==", + "requires": { + "clones": "^1.2.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "requires": { + "xmlchars": "^2.1.1" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + }, + "selfsigned": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz", + "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==", + "requires": { + "node-forge": "^0.10.0" + }, + "dependencies": { + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + } + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "serialize-to-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-1.2.2.tgz", + "integrity": "sha512-mUc8vA5iJghe+O+3s0YDGFLMJcqitVFk787YKiv8a4sf6RX5W0u81b+gcHrp15O0fFa010dRBVZvwcKXOWsL9Q==", + "requires": { + "js-beautify": "^1.8.9", + "safer-eval": "^1.3.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "shallow-copy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sockjs": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", + "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^3.4.0", + "websocket-driver": "^0.7.4" + } + }, + "sockjs-client": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.1.tgz", + "integrity": "sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ==", + "requires": { + "debug": "^3.2.6", + "eventsource": "^1.0.7", + "faye-websocket": "^0.11.3", + "inherits": "^2.0.4", + "json3": "^3.3.3", + "url-parse": "^1.5.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==" + }, + "spago": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/spago/-/spago-0.19.2.tgz", + "integrity": "sha512-/u4ofPqWkK1JKRlDU8ZpuLVEOqOpD7/F9zIms4jaPxrXDNhddvhZkbYXrFF/Pe4ZpawysrkhQxhKKt+FJfOfuw==", + "requires": { + "request": "^2.88.0", + "tar": "^4.4.8" + } + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "srcset": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-3.0.1.tgz", + "integrity": "sha512-MM8wDGg5BQJEj94tDrZDrX9wrC439/Eoeg3sgmVLPMjHgrAFeXAKk3tmFlCbKw5k+yOEhPXRpPlRcisQmqWVSQ==" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "static-eval": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.0.tgz", + "integrity": "sha512-agtxZ/kWSsCkI5E4QifRwsaPs0P0JmZV6dkLz6ILYfFYQGn+5plctanRN+IC8dJRiFkyXHrwEE3W9Wmx67uDbw==", + "requires": { + "escodegen": "^1.11.1" + }, + "dependencies": { + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + } + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "static-module": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/static-module/-/static-module-2.2.5.tgz", + "integrity": "sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ==", + "requires": { + "concat-stream": "~1.6.0", + "convert-source-map": "^1.5.1", + "duplexer2": "~0.1.4", + "escodegen": "~1.9.0", + "falafel": "^2.1.0", + "has": "^1.0.1", + "magic-string": "^0.22.4", + "merge-source-map": "1.0.4", + "object-inspect": "~1.4.0", + "quote-stream": "~1.0.2", + "readable-stream": "~2.3.3", + "shallow-copy": "~0.0.1", + "static-eval": "^2.0.0", + "through2": "~2.0.3" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "requires": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "requires": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "requires": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "tapable": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", + "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==" + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "terser": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz", + "integrity": "sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==", + "requires": { + "commander": "^2.19.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.10" + } + }, + "terser-webpack-plugin": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz", + "integrity": "sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==", + "requires": { + "jest-worker": "^27.0.2", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1", + "terser": "^5.7.0" + }, + "dependencies": { + "terser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz", + "integrity": "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==", + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + } + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" + }, + "timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" + }, + "tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "uncss": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/uncss/-/uncss-0.17.3.tgz", + "integrity": "sha512-ksdDWl81YWvF/X14fOSw4iu8tESDHFIeyKIeDrK6GEVTQvqJc1WlOEXqostNwOCi3qAj++4EaLsdAgPmUbEyog==", + "requires": { + "commander": "^2.20.0", + "glob": "^7.1.4", + "is-absolute-url": "^3.0.1", + "is-html": "^1.1.0", + "jsdom": "^14.1.0", + "lodash": "^4.17.15", + "postcss": "^7.0.17", + "postcss-selector-parser": "6.0.2", + "request": "^2.88.0" + }, + "dependencies": { + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==" + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + } + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==" + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", + "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==" + }, + "unicode-property-aliases-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", + "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==" + }, + "unicode-trie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz", + "integrity": "sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU=", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + }, + "uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "url-parse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", + "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + }, + "dependencies": { + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + } + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vlq": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", + "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==" + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "watchpack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", + "integrity": "sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==", + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "dependencies": { + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + } + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "webpack": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.40.0.tgz", + "integrity": "sha512-c7f5e/WWrxXWUzQqTBg54vBs5RgcAgpvKE4F4VegVgfo4x660ZxYUF2/hpMkZUnLjgytVTitjeXaN4IPlXCGIw==", + "requires": { + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.47", + "@webassemblyjs/ast": "1.11.0", + "@webassemblyjs/wasm-edit": "1.11.0", + "@webassemblyjs/wasm-parser": "1.11.0", + "acorn": "^8.2.1", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.8.0", + "es-module-lexer": "^0.6.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.4", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.2.0", + "webpack-sources": "^2.3.0" + }, + "dependencies": { + "acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==" + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + } + } + }, + "webpack-cli": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.7.2.tgz", + "integrity": "sha512-mEoLmnmOIZQNiRl0ebnjzQ74Hk0iKS5SiEEnpq3dRezoyR3yPaeQZCMCe+db4524pj1Pd5ghZXjT41KLzIhSLw==", + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.0.4", + "@webpack-cli/info": "^1.3.0", + "@webpack-cli/serve": "^1.5.1", + "colorette": "^1.2.1", + "commander": "^7.0.0", + "execa": "^5.0.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "v8-compile-cache": "^2.2.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", + "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", + "requires": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + } + } + }, + "webpack-dev-server": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz", + "integrity": "sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ==", + "requires": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.8", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==" + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "requires": { + "find-up": "^3.0.0" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "requires": { + "resolve-from": "^3.0.0" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.0.tgz", + "integrity": "sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ==", + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", + "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==" + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + } + } +} diff --git a/mlabs/demo/Main.hs b/mlabs/demo/Main.hs new file mode 100644 index 000000000..79eafb99a --- /dev/null +++ b/mlabs/demo/Main.hs @@ -0,0 +1,150 @@ +{-# OPTIONS_GHC -Wno-missing-import-lists #-} + +module Main (main) where + +-------------------------------------------------------------------------------- + +import GHC.Generics +import Prelude qualified as Hask + +-------------------------------------------------------------------------------- + +import Control.Monad (forM, forM_, void, when) +import Control.Monad.Freer (Eff, Member, interpret, reinterpret, type (~>)) +import Control.Monad.Freer.Error (Error, throwError) +import Control.Monad.Freer.Extras.Log (LogMsg, logDebug) +import Control.Monad.IO.Class (MonadIO (..)) +import Data.Aeson (FromJSON, Result (..), ToJSON, encode, fromJSON) +import Data.Bifunctor (Bifunctor (first)) +import Data.Default.Class +import Data.Map (Map) +import Data.Map qualified as Map +import Data.Row (type Empty, type (.\\)) +import Data.Semigroup qualified as Semigroup +import Prettyprinter (Pretty (..), viaShow) + +-------------------------------------------------------------------------------- + +import Cardano.Prelude qualified as Cardano +import Cardano.Wallet.Types qualified (WalletInfo (..)) +import Control.Concurrent.Availability qualified as Availability +import Plutus.Contract qualified as Contract +import Plutus.Contract.Effects.ExposeEndpoint qualified as Cardano +import Plutus.Contract.Resumable (Response) +import Plutus.Contract.Schema (Event, Handlers, Input, Output) +import Plutus.Contract.State (Contract, ContractRequest (..), ContractResponse (..)) +import Plutus.Contract.State qualified as Contract +import Plutus.PAB.Core qualified as PAB +import Plutus.PAB.Core.ContractInstance.STM qualified as Cardano +import Plutus.PAB.Effects.Contract (ContractEffect (..), PABContract (..)) +import Plutus.PAB.Effects.Contract.Builtin (Builtin, SomeBuiltin (..)) +import Plutus.PAB.Effects.Contract.Builtin qualified as Builtin +import Plutus.PAB.Events.Contract (ContractPABRequest) +import Plutus.PAB.Events.Contract qualified as Contract +import Plutus.PAB.Events.ContractInstanceState (PartiallyDecodedResponse) +import Plutus.PAB.Events.ContractInstanceState qualified as Contract +import Plutus.PAB.Monitoring.PABLogMsg (ContractEffectMsg (..), PABMultiAgentMsg (..)) +import Plutus.PAB.Simulator (Simulation, SimulatorContractHandler, SimulatorEffectHandlers) +import Plutus.PAB.Simulator qualified as Simulator +import Plutus.PAB.Types (PABError (..), WebserverConfig (..)) +import Plutus.PAB.Webserver.Server qualified as PAB +import Plutus.V1.Ledger.Ada qualified as Ada +import Plutus.V1.Ledger.Crypto qualified as Ledger +import Plutus.V1.Ledger.Slot qualified as Ledger (Slot (..)) +import Plutus.V1.Ledger.Value qualified as Ledger +import Plutus.V1.Ledger.Value qualified as Value +import PlutusTx.Prelude ((%)) +import PlutusTx.Prelude qualified as PlutusTx +import Wallet.Emulator.Types (Wallet (..), walletPubKey) +import Wallet.Emulator.Wallet qualified as Wallet + +-------------------------------------------------------------------------------- + +import Mlabs.Lending.Contract.Lendex qualified as Lendex +import Mlabs.Lending.Logic.Types (Coin, StartParams (..), UserAct (..), UserId (..)) +import Mlabs.Lending.Logic.Types qualified as Lendex + +-------------------------------------------------------------------------------- + +main :: IO () +main = void $ + Simulator.runSimulationWith handlers $ do + shutdown <- PAB.startServerDebug + + cidInit <- Simulator.activateContract (Wallet 1) Init + + -- The initial spend is enough to identify the entire market, provided the initial params are also clear. + -- TODO: get pool info here. + _ <- flip Simulator.waitForState cidInit $ \json -> case fromJSON json of + Success (Just (Semigroup.Last mkt)) -> Just mkt + _ -> Nothing + + shutdown + +data AavePAB + +data AaveContracts + = Init + | User Lendex.LendingPool + deriving stock (Show, Generic) + deriving anyclass (FromJSON, ToJSON) + +instance Pretty AaveContracts where + pretty = viaShow + +instance PABContract AavePAB where + type ContractDef AavePAB = AaveContracts + type State AavePAB = PartiallyDecodedResponse ContractPABRequest + + serialisableState _ = id + +handleLendexContract :: + ( Member (Error PABError) effs + , Member (LogMsg (PABMultiAgentMsg (Builtin AaveContracts))) effs + ) => + ContractEffect (Builtin AaveContracts) + ~> Eff effs +handleLendexContract = Builtin.handleBuiltin getSchema getContract + where + getSchema = \case + Init -> Builtin.endpointsToSchemas @Empty + User _ -> Builtin.endpointsToSchemas @Lendex.UserLendexSchema + getContract = \case + Init -> SomeBuiltin (Lendex.startLendex startParams) + User lendex -> SomeBuiltin (Lendex.userAction depositAct) + +handlers :: SimulatorEffectHandlers (Builtin AaveContracts) +handlers = + Simulator.mkSimulatorHandlers @(Builtin AaveContracts) [] $ + interpret handleLendexContract + +startParams :: StartParams +startParams = + StartParams + { sp'coins = [initCoinCfg] + , sp'initValue = initValue -- init value deposited to the lending app + } + +initValue :: Value.Value +initValue = Value.singleton Ada.adaSymbol Ada.adaToken 10000 + +-- TODO: figure out how to support multiple currencies +-- note: looks like we'll need a minimal minting contract to get currencies working, otherwise we can support Ada collateral, Ada borrow by removing `collateralNonBorrow uid asset` from the contract. +-- <> Value.Singleton () (Value.tokenName "USDc") + +initCoinCfg = + Lendex.CoinCfg + { coinCfg'coin = Value.AssetClass (Ada.adaSymbol, Ada.adaToken) + , coinCfg'rate = 1 % 1 + , coinCfg'aToken = Value.tokenName "aAda" + , coinCfg'interestModel = Lendex.defaultInterestModel + , coinCfg'liquidationBonus = 2 % 10 + } + +depositAct = + DepositAct + { act'amount = 100 + , act'asset = Value.AssetClass (Ada.adaSymbol, Ada.adaToken) + } + +-- -------------------------------------------------------------------------------- diff --git a/mlabs/deploy-app/Main.hs b/mlabs/deploy-app/Main.hs new file mode 100644 index 000000000..d9a49b3c8 --- /dev/null +++ b/mlabs/deploy-app/Main.hs @@ -0,0 +1,44 @@ +module Main where + +import PlutusTx.Prelude hiding (error) +import System.Environment (getArgs) +import System.Exit (die) +import Prelude (IO, String, error, print, undefined) + +import Mlabs.Emulator.Types (UserId (..)) +import Mlabs.NftStateMachine.Contract.Forge as F +import Mlabs.NftStateMachine.Contract.StateMachine as SM +import Mlabs.NftStateMachine.Logic.Types (Act (..), Nft (..), NftId (..), UserAct (..), initNft, toNftId) + +import Cardano.Api +import Cardano.Api.Shelley + +import Cardano.Ledger.Alonzo.Data qualified as Alonzo +import Codec.Serialise +import Ledger.Typed.Scripts.Validators as VS +import Plutus.V1.Ledger.Api (MintingPolicy, TxOutRef, Validator) +import Plutus.V1.Ledger.Api qualified as Plutus +import PlutusTx + +import Data.Aeson as Json +import Data.ByteString.Lazy qualified as LB +import Data.ByteString.Short qualified as SBS + +import Data.ByteString as DB +import Mlabs.Deploy.Governance +import Mlabs.Deploy.Nft + +main :: IO () +main = do + args <- getArgs + case args of + ["Nft"] -> + serializeNft + "56b4d636bfea5cb0628ba202214a9cca42997545da87dfd436e6e3d8d7ba3b28" + 0 + "4cebc6f2a3d0111ddeb09ac48e2053b83b33b15f29182f9b528c6491" + "MonaLisa" + "./../.github/workflows/nft_delivery" + ["Governance"] -> serializeGovernance + _ -> + die "Unknown deployment task type" diff --git a/mlabs/flake.lock b/mlabs/flake.lock new file mode 100644 index 000000000..d5a137d98 --- /dev/null +++ b/mlabs/flake.lock @@ -0,0 +1,1944 @@ +{ + "nodes": { + "HTTP": { + "flake": false, + "locked": { + "lastModified": 1451647621, + "narHash": "sha256-oHIyw3x0iKBexEo49YeUDV1k74ZtyYKGR2gNJXXRxts=", + "owner": "phadej", + "repo": "HTTP", + "rev": "9bc0996d412fef1787449d841277ef663ad9a915", + "type": "github" + }, + "original": { + "owner": "phadej", + "repo": "HTTP", + "type": "github" + } + }, + "HTTP_2": { + "flake": false, + "locked": { + "lastModified": 1451647621, + "narHash": "sha256-oHIyw3x0iKBexEo49YeUDV1k74ZtyYKGR2gNJXXRxts=", + "owner": "phadej", + "repo": "HTTP", + "rev": "9bc0996d412fef1787449d841277ef663ad9a915", + "type": "github" + }, + "original": { + "owner": "phadej", + "repo": "HTTP", + "type": "github" + } + }, + "HTTP_3": { + "flake": false, + "locked": { + "lastModified": 1451647621, + "narHash": "sha256-oHIyw3x0iKBexEo49YeUDV1k74ZtyYKGR2gNJXXRxts=", + "owner": "phadej", + "repo": "HTTP", + "rev": "9bc0996d412fef1787449d841277ef663ad9a915", + "type": "github" + }, + "original": { + "owner": "phadej", + "repo": "HTTP", + "type": "github" + } + }, + "Win32-network": { + "flake": false, + "locked": { + "lastModified": 1627315969, + "narHash": "sha256-Hesb5GXSx0IwKSIi42ofisVELcQNX6lwHcoZcbaDiqc=", + "owner": "input-output-hk", + "repo": "Win32-network", + "rev": "3825d3abf75f83f406c1f7161883c438dac7277d", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "Win32-network", + "rev": "3825d3abf75f83f406c1f7161883c438dac7277d", + "type": "github" + } + }, + "Win32-network_2": { + "flake": false, + "locked": { + "lastModified": 1627315969, + "narHash": "sha256-Hesb5GXSx0IwKSIi42ofisVELcQNX6lwHcoZcbaDiqc=", + "owner": "input-output-hk", + "repo": "Win32-network", + "rev": "3825d3abf75f83f406c1f7161883c438dac7277d", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "Win32-network", + "rev": "3825d3abf75f83f406c1f7161883c438dac7277d", + "type": "github" + } + }, + "bot-plutus-interface": { + "inputs": { + "haskell-nix": "haskell-nix_2", + "nixpkgs": [ + "haskell-nix", + "nixpkgs-unstable" + ], + "plutus": "plutus" + }, + "locked": { + "lastModified": 1645019043, + "narHash": "sha256-J2tvNMubOPwMeUNy0+EyNjrXVhBnWIE5iJpk70VtD/g=", + "owner": "mlabs-haskell", + "repo": "bot-plutus-interface", + "rev": "9315a8210bb273ab69e17899ef5de4f5fbb195e5", + "type": "github" + }, + "original": { + "owner": "mlabs-haskell", + "repo": "bot-plutus-interface", + "rev": "9315a8210bb273ab69e17899ef5de4f5fbb195e5", + "type": "github" + } + }, + "cabal-32": { + "flake": false, + "locked": { + "lastModified": 1603716527, + "narHash": "sha256-sDbrmur9Zfp4mPKohCD8IDZfXJ0Tjxpmr2R+kg5PpSY=", + "owner": "haskell", + "repo": "cabal", + "rev": "94aaa8e4720081f9c75497e2735b90f6a819b08e", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.2", + "repo": "cabal", + "type": "github" + } + }, + "cabal-32_2": { + "flake": false, + "locked": { + "lastModified": 1603716527, + "narHash": "sha256-sDbrmur9Zfp4mPKohCD8IDZfXJ0Tjxpmr2R+kg5PpSY=", + "owner": "haskell", + "repo": "cabal", + "rev": "94aaa8e4720081f9c75497e2735b90f6a819b08e", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.2", + "repo": "cabal", + "type": "github" + } + }, + "cabal-32_3": { + "flake": false, + "locked": { + "lastModified": 1603716527, + "narHash": "sha256-sDbrmur9Zfp4mPKohCD8IDZfXJ0Tjxpmr2R+kg5PpSY=", + "owner": "haskell", + "repo": "cabal", + "rev": "94aaa8e4720081f9c75497e2735b90f6a819b08e", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.2", + "repo": "cabal", + "type": "github" + } + }, + "cabal-34": { + "flake": false, + "locked": { + "lastModified": 1622475795, + "narHash": "sha256-chwTL304Cav+7p38d9mcb+egABWmxo2Aq+xgVBgEb/U=", + "owner": "haskell", + "repo": "cabal", + "rev": "b086c1995cdd616fc8d91f46a21e905cc50a1049", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.4", + "repo": "cabal", + "type": "github" + } + }, + "cabal-34_2": { + "flake": false, + "locked": { + "lastModified": 1622475795, + "narHash": "sha256-chwTL304Cav+7p38d9mcb+egABWmxo2Aq+xgVBgEb/U=", + "owner": "haskell", + "repo": "cabal", + "rev": "b086c1995cdd616fc8d91f46a21e905cc50a1049", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.4", + "repo": "cabal", + "type": "github" + } + }, + "cabal-34_3": { + "flake": false, + "locked": { + "lastModified": 1622475795, + "narHash": "sha256-chwTL304Cav+7p38d9mcb+egABWmxo2Aq+xgVBgEb/U=", + "owner": "haskell", + "repo": "cabal", + "rev": "b086c1995cdd616fc8d91f46a21e905cc50a1049", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.4", + "repo": "cabal", + "type": "github" + } + }, + "cabal-36": { + "flake": false, + "locked": { + "lastModified": 1640163203, + "narHash": "sha256-TwDWP2CffT0j40W6zr0J1Qbu+oh3nsF1lUx9446qxZM=", + "owner": "haskell", + "repo": "cabal", + "rev": "ecf418050c1821f25e2e218f1be94c31e0465df1", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.6", + "repo": "cabal", + "type": "github" + } + }, + "cardano-addresses": { + "flake": false, + "locked": { + "lastModified": 1631515399, + "narHash": "sha256-XgXQKJHRKAFwIjONh19D/gKE0ARlhMXXcV74eZpd0lw=", + "owner": "input-output-hk", + "repo": "cardano-addresses", + "rev": "d2f86caa085402a953920c6714a0de6a50b655ec", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-addresses", + "rev": "d2f86caa085402a953920c6714a0de6a50b655ec", + "type": "github" + } + }, + "cardano-addresses_2": { + "flake": false, + "locked": { + "lastModified": 1631515399, + "narHash": "sha256-XgXQKJHRKAFwIjONh19D/gKE0ARlhMXXcV74eZpd0lw=", + "owner": "input-output-hk", + "repo": "cardano-addresses", + "rev": "d2f86caa085402a953920c6714a0de6a50b655ec", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-addresses", + "rev": "d2f86caa085402a953920c6714a0de6a50b655ec", + "type": "github" + } + }, + "cardano-base": { + "flake": false, + "locked": { + "lastModified": 1633088283, + "narHash": "sha256-JKpOlruMX5sr9eaQ3AuOppCbBjQIRKwF4ny20tdPnUg=", + "owner": "input-output-hk", + "repo": "cardano-base", + "rev": "654f5b7c76f7cc57900b4ddc664a82fc3b925fb0", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-base", + "rev": "654f5b7c76f7cc57900b4ddc664a82fc3b925fb0", + "type": "github" + } + }, + "cardano-base_2": { + "flake": false, + "locked": { + "lastModified": 1633088283, + "narHash": "sha256-JKpOlruMX5sr9eaQ3AuOppCbBjQIRKwF4ny20tdPnUg=", + "owner": "input-output-hk", + "repo": "cardano-base", + "rev": "654f5b7c76f7cc57900b4ddc664a82fc3b925fb0", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-base", + "rev": "654f5b7c76f7cc57900b4ddc664a82fc3b925fb0", + "type": "github" + } + }, + "cardano-config": { + "flake": false, + "locked": { + "lastModified": 1634339627, + "narHash": "sha256-jQbwcfNJ8am7Q3W+hmTFmyo3wp3QItquEH//klNiofI=", + "owner": "input-output-hk", + "repo": "cardano-config", + "rev": "e9de7a2cf70796f6ff26eac9f9540184ded0e4e6", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-config", + "rev": "e9de7a2cf70796f6ff26eac9f9540184ded0e4e6", + "type": "github" + } + }, + "cardano-crypto": { + "flake": false, + "locked": { + "lastModified": 1604244485, + "narHash": "sha256-2Fipex/WjIRMrvx6F3hjJoAeMtFd2wGnZECT0kuIB9k=", + "owner": "input-output-hk", + "repo": "cardano-crypto", + "rev": "f73079303f663e028288f9f4a9e08bcca39a923e", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-crypto", + "rev": "f73079303f663e028288f9f4a9e08bcca39a923e", + "type": "github" + } + }, + "cardano-crypto_2": { + "flake": false, + "locked": { + "lastModified": 1604244485, + "narHash": "sha256-2Fipex/WjIRMrvx6F3hjJoAeMtFd2wGnZECT0kuIB9k=", + "owner": "input-output-hk", + "repo": "cardano-crypto", + "rev": "f73079303f663e028288f9f4a9e08bcca39a923e", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-crypto", + "rev": "f73079303f663e028288f9f4a9e08bcca39a923e", + "type": "github" + } + }, + "cardano-ledger": { + "flake": false, + "locked": { + "lastModified": 1634701482, + "narHash": "sha256-HTPOmVOXgBD/3uAxZip/HSttaKcJ+uImYDbuwANAw1c=", + "owner": "input-output-hk", + "repo": "cardano-ledger", + "rev": "bf008ce028751cae9fb0b53c3bef20f07c06e333", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-ledger", + "rev": "bf008ce028751cae9fb0b53c3bef20f07c06e333", + "type": "github" + } + }, + "cardano-ledger_2": { + "flake": false, + "locked": { + "lastModified": 1634701482, + "narHash": "sha256-HTPOmVOXgBD/3uAxZip/HSttaKcJ+uImYDbuwANAw1c=", + "owner": "input-output-hk", + "repo": "cardano-ledger", + "rev": "bf008ce028751cae9fb0b53c3bef20f07c06e333", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-ledger", + "rev": "bf008ce028751cae9fb0b53c3bef20f07c06e333", + "type": "github" + } + }, + "cardano-node": { + "inputs": { + "customConfig": "customConfig", + "haskellNix": "haskellNix", + "iohkNix": "iohkNix", + "nixpkgs": [ + "nixpkgs" + ], + "utils": "utils" + }, + "locked": { + "lastModified": 1638955893, + "narHash": "sha256-PWcWv2RKsxHrsDs+ZjNeCOJlfmIW9CGilPA+UDN2aQI=", + "owner": "input-output-hk", + "repo": "cardano-node", + "rev": "4f65fb9a27aa7e3a1873ab4211e412af780a3648", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-node", + "rev": "4f65fb9a27aa7e3a1873ab4211e412af780a3648", + "type": "github" + } + }, + "cardano-prelude": { + "flake": false, + "locked": { + "lastModified": 1617089317, + "narHash": "sha256-kgX3DKyfjBb8/XcDEd+/adlETsFlp5sCSurHWgsFAQI=", + "owner": "input-output-hk", + "repo": "cardano-prelude", + "rev": "bb4ed71ba8e587f672d06edf9d2e376f4b055555", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-prelude", + "rev": "bb4ed71ba8e587f672d06edf9d2e376f4b055555", + "type": "github" + } + }, + "cardano-prelude_2": { + "flake": false, + "locked": { + "lastModified": 1617089317, + "narHash": "sha256-kgX3DKyfjBb8/XcDEd+/adlETsFlp5sCSurHWgsFAQI=", + "owner": "input-output-hk", + "repo": "cardano-prelude", + "rev": "bb4ed71ba8e587f672d06edf9d2e376f4b055555", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-prelude", + "rev": "bb4ed71ba8e587f672d06edf9d2e376f4b055555", + "type": "github" + } + }, + "cardano-repo-tool": { + "flake": false, + "locked": { + "lastModified": 1624584417, + "narHash": "sha256-YSepT97PagR/1jTYV/Yer8a2GjFe9+tTwaTCHxuK50M=", + "owner": "input-output-hk", + "repo": "cardano-repo-tool", + "rev": "30e826ed8f00e3e154453b122a6f3d779b2f73ec", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-repo-tool", + "type": "github" + } + }, + "cardano-shell": { + "flake": false, + "locked": { + "lastModified": 1608537748, + "narHash": "sha256-PulY1GfiMgKVnBci3ex4ptk2UNYMXqGjJOxcPy2KYT4=", + "owner": "input-output-hk", + "repo": "cardano-shell", + "rev": "9392c75087cb9a3d453998f4230930dea3a95725", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-shell", + "type": "github" + } + }, + "cardano-shell_2": { + "flake": false, + "locked": { + "lastModified": 1608537748, + "narHash": "sha256-PulY1GfiMgKVnBci3ex4ptk2UNYMXqGjJOxcPy2KYT4=", + "owner": "input-output-hk", + "repo": "cardano-shell", + "rev": "9392c75087cb9a3d453998f4230930dea3a95725", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-shell", + "type": "github" + } + }, + "cardano-shell_3": { + "flake": false, + "locked": { + "lastModified": 1608537748, + "narHash": "sha256-PulY1GfiMgKVnBci3ex4ptk2UNYMXqGjJOxcPy2KYT4=", + "owner": "input-output-hk", + "repo": "cardano-shell", + "rev": "9392c75087cb9a3d453998f4230930dea3a95725", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-shell", + "type": "github" + } + }, + "cardano-wallet": { + "flake": false, + "locked": { + "lastModified": 1639607349, + "narHash": "sha256-JuYH5pAF7gOsliES0Beo86PinoBmmKXWShXT3NqVlgQ=", + "owner": "j-mueller", + "repo": "cardano-wallet", + "rev": "760140e238a5fbca61d1b286d7a80ece058dc729", + "type": "github" + }, + "original": { + "owner": "j-mueller", + "repo": "cardano-wallet", + "rev": "760140e238a5fbca61d1b286d7a80ece058dc729", + "type": "github" + } + }, + "cardano-wallet_2": { + "flake": false, + "locked": { + "lastModified": 1639607349, + "narHash": "sha256-JuYH5pAF7gOsliES0Beo86PinoBmmKXWShXT3NqVlgQ=", + "owner": "input-output-hk", + "repo": "cardano-wallet", + "rev": "760140e238a5fbca61d1b286d7a80ece058dc729", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-wallet", + "rev": "760140e238a5fbca61d1b286d7a80ece058dc729", + "type": "github" + } + }, + "customConfig": { + "locked": { + "lastModified": 1630400035, + "narHash": "sha256-MWaVOCzuFwp09wZIW9iHq5wWen5C69I940N1swZLEQ0=", + "owner": "input-output-hk", + "repo": "empty-flake", + "rev": "2040a05b67bf9a669ce17eca56beb14b4206a99a", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "empty-flake", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1641205782, + "narHash": "sha256-4jY7RCWUoZ9cKD8co0/4tFARpWB+57+r1bLLvXNJliY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "b7547d3eed6f32d06102ead8991ec52ab0a4f1a7", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1641205782, + "narHash": "sha256-4jY7RCWUoZ9cKD8co0/4tFARpWB+57+r1bLLvXNJliY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "b7547d3eed6f32d06102ead8991ec52ab0a4f1a7", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1623875721, + "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1623875721, + "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "locked": { + "lastModified": 1623875721, + "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flat": { + "flake": false, + "locked": { + "lastModified": 1628771504, + "narHash": "sha256-lRFND+ZnZvAph6ZYkr9wl9VAx41pb3uSFP8Wc7idP9M=", + "owner": "input-output-hk", + "repo": "flat", + "rev": "ee59880f47ab835dbd73bea0847dab7869fc20d8", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "flat", + "rev": "ee59880f47ab835dbd73bea0847dab7869fc20d8", + "type": "github" + } + }, + "flat_2": { + "flake": false, + "locked": { + "lastModified": 1628771504, + "narHash": "sha256-lRFND+ZnZvAph6ZYkr9wl9VAx41pb3uSFP8Wc7idP9M=", + "owner": "input-output-hk", + "repo": "flat", + "rev": "ee59880f47ab835dbd73bea0847dab7869fc20d8", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "flat", + "rev": "ee59880f47ab835dbd73bea0847dab7869fc20d8", + "type": "github" + } + }, + "ghc-8.6.5-iohk": { + "flake": false, + "locked": { + "lastModified": 1600920045, + "narHash": "sha256-DO6kxJz248djebZLpSzTGD6s8WRpNI9BTwUeOf5RwY8=", + "owner": "input-output-hk", + "repo": "ghc", + "rev": "95713a6ecce4551240da7c96b6176f980af75cae", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "ref": "release/8.6.5-iohk", + "repo": "ghc", + "type": "github" + } + }, + "ghc-8.6.5-iohk_2": { + "flake": false, + "locked": { + "lastModified": 1600920045, + "narHash": "sha256-DO6kxJz248djebZLpSzTGD6s8WRpNI9BTwUeOf5RwY8=", + "owner": "input-output-hk", + "repo": "ghc", + "rev": "95713a6ecce4551240da7c96b6176f980af75cae", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "ref": "release/8.6.5-iohk", + "repo": "ghc", + "type": "github" + } + }, + "ghc-8.6.5-iohk_3": { + "flake": false, + "locked": { + "lastModified": 1600920045, + "narHash": "sha256-DO6kxJz248djebZLpSzTGD6s8WRpNI9BTwUeOf5RwY8=", + "owner": "input-output-hk", + "repo": "ghc", + "rev": "95713a6ecce4551240da7c96b6176f980af75cae", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "ref": "release/8.6.5-iohk", + "repo": "ghc", + "type": "github" + } + }, + "gitignore-nix": { + "flake": false, + "locked": { + "lastModified": 1611672876, + "narHash": "sha256-qHu3uZ/o9jBHiA3MEKHJ06k7w4heOhA+4HCSIvflRxo=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "211907489e9f198594c0eb0ca9256a1949c9d412", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "goblins": { + "flake": false, + "locked": { + "lastModified": 1598362523, + "narHash": "sha256-z9ut0y6umDIjJIRjz9KSvKgotuw06/S8QDwOtVdGiJ0=", + "owner": "input-output-hk", + "repo": "goblins", + "rev": "cde90a2b27f79187ca8310b6549331e59595e7ba", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "goblins", + "rev": "cde90a2b27f79187ca8310b6549331e59595e7ba", + "type": "github" + } + }, + "goblins_2": { + "flake": false, + "locked": { + "lastModified": 1598362523, + "narHash": "sha256-z9ut0y6umDIjJIRjz9KSvKgotuw06/S8QDwOtVdGiJ0=", + "owner": "input-output-hk", + "repo": "goblins", + "rev": "cde90a2b27f79187ca8310b6549331e59595e7ba", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "goblins", + "rev": "cde90a2b27f79187ca8310b6549331e59595e7ba", + "type": "github" + } + }, + "hackage": { + "flake": false, + "locked": { + "lastModified": 1631668346, + "narHash": "sha256-4dWzl+HoFlXNhaqw4snC3ELBU+6IVBUihuVz41JZi4Y=", + "owner": "input-output-hk", + "repo": "hackage.nix", + "rev": "7eb138fdad1ce0a3fba0697d3eba819b185a83d6", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "hackage.nix", + "type": "github" + } + }, + "hackage-nix": { + "flake": false, + "locked": { + "lastModified": 1637291070, + "narHash": "sha256-hTX2Xo36i9MR6PNwA/89C8daKjxmx5ZS5lwR2Cbp8Yo=", + "owner": "input-output-hk", + "repo": "hackage.nix", + "rev": "6ea4ad5f4a5e2303cd64974329ba90ccc410a012", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "hackage.nix", + "type": "github" + } + }, + "hackage_2": { + "flake": false, + "locked": { + "lastModified": 1642554756, + "narHash": "sha256-1+SN+z80HgKYshlCf8dRxwRojQzuwwsQ5uq14N/JP1Y=", + "owner": "input-output-hk", + "repo": "hackage.nix", + "rev": "f9d5e67ca90926b244c0ad68815371d37582a149", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "hackage.nix", + "type": "github" + } + }, + "hackage_3": { + "flake": false, + "locked": { + "lastModified": 1638842221, + "narHash": "sha256-xy9Pk/SiYSfwU6Qolu+AWzXlSktKL/v6kJvng4gosrA=", + "owner": "input-output-hk", + "repo": "hackage.nix", + "rev": "0d5a13378159f6574e9b3e28b65fc0f2dd4a91e4", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "hackage.nix", + "type": "github" + } + }, + "haskell-language-server": { + "flake": false, + "locked": { + "lastModified": 1638136578, + "narHash": "sha256-Reo9BQ12O+OX7tuRfaDPZPBpJW4jnxZetm63BxYncoM=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "745ef26f406dbdd5e4a538585f8519af9f1ccb09", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "1.5.1", + "repo": "haskell-language-server", + "type": "github" + } + }, + "haskell-nix": { + "inputs": { + "HTTP": "HTTP_2", + "cabal-32": "cabal-32_2", + "cabal-34": "cabal-34_2", + "cabal-36": "cabal-36", + "cardano-shell": "cardano-shell_2", + "flake-utils": "flake-utils_2", + "ghc-8.6.5-iohk": "ghc-8.6.5-iohk_2", + "hackage": "hackage_2", + "hpc-coveralls": "hpc-coveralls_2", + "nix-tools": "nix-tools_2", + "nixpkgs": [ + "haskell-nix", + "nixpkgs-unstable" + ], + "nixpkgs-2003": "nixpkgs-2003_2", + "nixpkgs-2105": "nixpkgs-2105_2", + "nixpkgs-2111": "nixpkgs-2111", + "nixpkgs-unstable": "nixpkgs-unstable_2", + "old-ghc-nix": "old-ghc-nix_2", + "stackage": "stackage_2" + }, + "locked": { + "lastModified": 1642811877, + "narHash": "sha256-7YbbFF4ISWMcs5hHDfH7GkCSccvwEwhvKZ5D74Cuajo=", + "owner": "L-as", + "repo": "haskell.nix", + "rev": "ac825b91c202947ec59b1a477003564cc018fcec", + "type": "github" + }, + "original": { + "owner": "L-as", + "repo": "haskell.nix", + "rev": "ac825b91c202947ec59b1a477003564cc018fcec", + "type": "github" + } + }, + "haskell-nix_2": { + "inputs": { + "HTTP": "HTTP_3", + "cabal-32": "cabal-32_3", + "cabal-34": "cabal-34_3", + "cardano-shell": "cardano-shell_3", + "flake-utils": "flake-utils_3", + "ghc-8.6.5-iohk": "ghc-8.6.5-iohk_3", + "hackage": "hackage_3", + "hpc-coveralls": "hpc-coveralls_3", + "nix-tools": "nix-tools_3", + "nixpkgs": [ + "plutip", + "bot-plutus-interface", + "haskell-nix", + "nixpkgs-2105" + ], + "nixpkgs-2003": "nixpkgs-2003_3", + "nixpkgs-2105": "nixpkgs-2105_3", + "nixpkgs-2111": "nixpkgs-2111_2", + "nixpkgs-unstable": "nixpkgs-unstable_3", + "old-ghc-nix": "old-ghc-nix_3", + "stackage": "stackage_3" + }, + "locked": { + "lastModified": 1638842356, + "narHash": "sha256-hYm3bJ+Fik2ZDusQlUuJjFlKHdFNWPHNmePLbXhtQ0U=", + "owner": "input-output-hk", + "repo": "haskell.nix", + "rev": "e0d8b052c0a7326b6064d99c96417a4f572b8867", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "haskell.nix", + "type": "github" + } + }, + "haskell-nix_3": { + "flake": false, + "locked": { + "lastModified": 1629380841, + "narHash": "sha256-gWOWCfX7IgVSvMMYN6rBGK6EA0pk6pmYguXzMvGte+Q=", + "owner": "input-output-hk", + "repo": "haskell.nix", + "rev": "7215f083b37741446aa325b20c8ba9f9f76015eb", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "haskell.nix", + "type": "github" + } + }, + "haskellNix": { + "inputs": { + "HTTP": "HTTP", + "cabal-32": "cabal-32", + "cabal-34": "cabal-34", + "cardano-shell": "cardano-shell", + "flake-utils": "flake-utils", + "ghc-8.6.5-iohk": "ghc-8.6.5-iohk", + "hackage": "hackage", + "hpc-coveralls": "hpc-coveralls", + "nix-tools": "nix-tools", + "nixpkgs": [ + "cardano-node", + "nixpkgs" + ], + "nixpkgs-2003": "nixpkgs-2003", + "nixpkgs-2009": "nixpkgs-2009", + "nixpkgs-2105": "nixpkgs-2105", + "nixpkgs-unstable": "nixpkgs-unstable", + "old-ghc-nix": "old-ghc-nix", + "stackage": "stackage" + }, + "locked": { + "lastModified": 1631703614, + "narHash": "sha256-XYC0M96V9oQTGq1TSIQTVwkA+SrUqE4o6kUZo4iO8Z4=", + "owner": "input-output-hk", + "repo": "haskell.nix", + "rev": "19052d83fda811dd39216e3fc197c980abd037fd", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "haskell.nix", + "type": "github" + } + }, + "hpc-coveralls": { + "flake": false, + "locked": { + "lastModified": 1607498076, + "narHash": "sha256-8uqsEtivphgZWYeUo5RDUhp6bO9j2vaaProQxHBltQk=", + "owner": "sevanspowell", + "repo": "hpc-coveralls", + "rev": "14df0f7d229f4cd2e79f8eabb1a740097fdfa430", + "type": "github" + }, + "original": { + "owner": "sevanspowell", + "repo": "hpc-coveralls", + "type": "github" + } + }, + "hpc-coveralls_2": { + "flake": false, + "locked": { + "lastModified": 1607498076, + "narHash": "sha256-8uqsEtivphgZWYeUo5RDUhp6bO9j2vaaProQxHBltQk=", + "owner": "sevanspowell", + "repo": "hpc-coveralls", + "rev": "14df0f7d229f4cd2e79f8eabb1a740097fdfa430", + "type": "github" + }, + "original": { + "owner": "sevanspowell", + "repo": "hpc-coveralls", + "type": "github" + } + }, + "hpc-coveralls_3": { + "flake": false, + "locked": { + "lastModified": 1607498076, + "narHash": "sha256-8uqsEtivphgZWYeUo5RDUhp6bO9j2vaaProQxHBltQk=", + "owner": "sevanspowell", + "repo": "hpc-coveralls", + "rev": "14df0f7d229f4cd2e79f8eabb1a740097fdfa430", + "type": "github" + }, + "original": { + "owner": "sevanspowell", + "repo": "hpc-coveralls", + "type": "github" + } + }, + "iohk-monitoring-framework": { + "flake": false, + "locked": { + "lastModified": 1624367860, + "narHash": "sha256-QE3QRpIHIABm+qCP/wP4epbUx0JmSJ9BMePqWEd3iMY=", + "owner": "input-output-hk", + "repo": "iohk-monitoring-framework", + "rev": "46f994e216a1f8b36fe4669b47b2a7011b0e153c", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "iohk-monitoring-framework", + "rev": "46f994e216a1f8b36fe4669b47b2a7011b0e153c", + "type": "github" + } + }, + "iohk-monitoring-framework_2": { + "flake": false, + "locked": { + "lastModified": 1624367860, + "narHash": "sha256-QE3QRpIHIABm+qCP/wP4epbUx0JmSJ9BMePqWEd3iMY=", + "owner": "input-output-hk", + "repo": "iohk-monitoring-framework", + "rev": "46f994e216a1f8b36fe4669b47b2a7011b0e153c", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "iohk-monitoring-framework", + "rev": "46f994e216a1f8b36fe4669b47b2a7011b0e153c", + "type": "github" + } + }, + "iohk-nix": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1645693195, + "narHash": "sha256-UDemE2MFEi/L8Xmwi1/FuKU9ka3QqDye6k7rVW6ryeE=", + "owner": "input-output-hk", + "repo": "iohk-nix", + "rev": "29c9a3b6704b5c0df3bb4a3e65240749883c50a0", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "iohk-nix", + "type": "github" + } + }, + "iohk-nix_2": { + "flake": false, + "locked": { + "lastModified": 1626953580, + "narHash": "sha256-iEI9aTOaZMGsjWzcrctrC0usmiagwKT2v1LSDe9/tMU=", + "owner": "input-output-hk", + "repo": "iohk-nix", + "rev": "cbd497f5844249ef8fe617166337d59f2a6ebe90", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "iohk-nix", + "type": "github" + } + }, + "iohk-nix_3": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1643251385, + "narHash": "sha256-Czbd69lg0ARSZfC18V6h+gtPMioWDAEVPbiHgL2x9LM=", + "owner": "input-output-hk", + "repo": "iohk-nix", + "rev": "9d6ee3dcb3482f791e40ed991ad6fc649b343ad4", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "iohk-nix", + "type": "github" + } + }, + "iohkNix": { + "inputs": { + "nixpkgs": [ + "cardano-node", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1631778944, + "narHash": "sha256-N5eCcUYtZ5kUOl/JJGjx6ZzhA3uIn1itDRTiRV+3jLw=", + "owner": "input-output-hk", + "repo": "iohk-nix", + "rev": "db2c75a09c696271194bb3ef25ec8e9839b594b7", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "iohk-nix", + "type": "github" + } + }, + "nix-tools": { + "flake": false, + "locked": { + "lastModified": 1626997434, + "narHash": "sha256-1judQmP298ao6cGUNxcGhcAXHOnA9qSLvWk/ZtoUL7w=", + "owner": "input-output-hk", + "repo": "nix-tools", + "rev": "c8c5e6a6fbb12a73598d1a434984a36e880ce3cf", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "nix-tools", + "type": "github" + } + }, + "nix-tools_2": { + "flake": false, + "locked": { + "lastModified": 1636018067, + "narHash": "sha256-ng306fkuwr6V/malWtt3979iAC4yMVDDH2ViwYB6sQE=", + "owner": "input-output-hk", + "repo": "nix-tools", + "rev": "ed5bd7215292deba55d6ab7a4e8c21f8b1564dda", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "nix-tools", + "type": "github" + } + }, + "nix-tools_3": { + "flake": false, + "locked": { + "lastModified": 1636018067, + "narHash": "sha256-ng306fkuwr6V/malWtt3979iAC4yMVDDH2ViwYB6sQE=", + "owner": "input-output-hk", + "repo": "nix-tools", + "rev": "ed5bd7215292deba55d6ab7a4e8c21f8b1564dda", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "nix-tools", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1645623357, + "narHash": "sha256-vAaI91QFn/kY/uMiebW+kG2mPmxirMSJWYtkqkBKdDc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9222ae36b208d1c6b55d88e10aa68f969b5b5244", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs-2003": { + "locked": { + "lastModified": 1620055814, + "narHash": "sha256-8LEHoYSJiL901bTMVatq+rf8y7QtWuZhwwpKE2fyaRY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1db42b7fe3878f3f5f7a4f2dc210772fd080e205", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-20.03-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2003_2": { + "locked": { + "lastModified": 1620055814, + "narHash": "sha256-8LEHoYSJiL901bTMVatq+rf8y7QtWuZhwwpKE2fyaRY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1db42b7fe3878f3f5f7a4f2dc210772fd080e205", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-20.03-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2003_3": { + "locked": { + "lastModified": 1620055814, + "narHash": "sha256-8LEHoYSJiL901bTMVatq+rf8y7QtWuZhwwpKE2fyaRY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1db42b7fe3878f3f5f7a4f2dc210772fd080e205", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-20.03-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2009": { + "locked": { + "lastModified": 1624271064, + "narHash": "sha256-qns/uRW7MR2EfVf6VEeLgCsCp7pIOjDeR44JzTF09MA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "46d1c3f28ca991601a53e9a14fdd53fcd3dd8416", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-20.09-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2105": { + "locked": { + "lastModified": 1630481079, + "narHash": "sha256-leWXLchbAbqOlLT6tju631G40SzQWPqaAXQG3zH1Imw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "110a2c9ebbf5d4a94486854f18a37a938cfacbbb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-21.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2105_2": { + "locked": { + "lastModified": 1640283157, + "narHash": "sha256-6Ddfop+rKE+Gl9Tjp9YIrkfoYPzb8F80ergdjcq3/MY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "dde1557825c5644c869c5efc7448dc03722a8f09", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-21.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2105_3": { + "locked": { + "lastModified": 1630481079, + "narHash": "sha256-leWXLchbAbqOlLT6tju631G40SzQWPqaAXQG3zH1Imw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "110a2c9ebbf5d4a94486854f18a37a938cfacbbb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-21.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2111": { + "locked": { + "lastModified": 1640283207, + "narHash": "sha256-SCwl7ZnCfMDsuSYvwIroiAlk7n33bW8HFfY8NvKhcPA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "64c7e3388bbd9206e437713351e814366e0c3284", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-21.11-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2111_2": { + "locked": { + "lastModified": 1638410074, + "narHash": "sha256-MQYI4k4XkoTzpeRjq5wl+1NShsl1CKq8MISFuZ81sWs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5b80f23502f8e902612a8c631dfce383e1c56596", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-21.11-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1628785280, + "narHash": "sha256-2B5eMrEr6O8ff2aQNeVxTB+9WrGE80OB4+oM6T7fOcc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6525bbc06a39f26750ad8ee0d40000ddfdc24acb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable_2": { + "locked": { + "lastModified": 1641285291, + "narHash": "sha256-KYaOBNGar3XWTxTsYPr9P6u74KAqNq0wobEC236U+0c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0432195a4b8d68faaa7d3d4b355260a3120aeeae", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable_3": { + "locked": { + "lastModified": 1635295995, + "narHash": "sha256-sGYiXjFlxTTMNb4NSkgvX+knOOTipE6gqwPUQpxNF+c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "22a500a3f87bbce73bd8d777ef920b43a636f018", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "flake": false, + "locked": { + "lastModified": 1628785280, + "narHash": "sha256-2B5eMrEr6O8ff2aQNeVxTB+9WrGE80OB4+oM6T7fOcc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6525bbc06a39f26750ad8ee0d40000ddfdc24acb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1644486793, + "narHash": "sha256-EeijR4guVHgVv+JpOX3cQO+1XdrkJfGmiJ9XVsVU530=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1882c6b7368fd284ad01b0a5b5601ef136321292", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "old-ghc-nix": { + "flake": false, + "locked": { + "lastModified": 1631092763, + "narHash": "sha256-sIKgO+z7tj4lw3u6oBZxqIhDrzSkvpHtv0Kki+lh9Fg=", + "owner": "angerman", + "repo": "old-ghc-nix", + "rev": "af48a7a7353e418119b6dfe3cd1463a657f342b8", + "type": "github" + }, + "original": { + "owner": "angerman", + "ref": "master", + "repo": "old-ghc-nix", + "type": "github" + } + }, + "old-ghc-nix_2": { + "flake": false, + "locked": { + "lastModified": 1631092763, + "narHash": "sha256-sIKgO+z7tj4lw3u6oBZxqIhDrzSkvpHtv0Kki+lh9Fg=", + "owner": "angerman", + "repo": "old-ghc-nix", + "rev": "af48a7a7353e418119b6dfe3cd1463a657f342b8", + "type": "github" + }, + "original": { + "owner": "angerman", + "ref": "master", + "repo": "old-ghc-nix", + "type": "github" + } + }, + "old-ghc-nix_3": { + "flake": false, + "locked": { + "lastModified": 1631092763, + "narHash": "sha256-sIKgO+z7tj4lw3u6oBZxqIhDrzSkvpHtv0Kki+lh9Fg=", + "owner": "angerman", + "repo": "old-ghc-nix", + "rev": "af48a7a7353e418119b6dfe3cd1463a657f342b8", + "type": "github" + }, + "original": { + "owner": "angerman", + "ref": "master", + "repo": "old-ghc-nix", + "type": "github" + } + }, + "optparse-applicative": { + "flake": false, + "locked": { + "lastModified": 1628901899, + "narHash": "sha256-uQx+SEYsCH7JcG3xAT0eJck9yq3y0cvx49bvItLLer8=", + "owner": "input-output-hk", + "repo": "optparse-applicative", + "rev": "7497a29cb998721a9068d5725d49461f2bba0e7a", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "optparse-applicative", + "rev": "7497a29cb998721a9068d5725d49461f2bba0e7a", + "type": "github" + } + }, + "optparse-applicative_2": { + "flake": false, + "locked": { + "lastModified": 1628901899, + "narHash": "sha256-uQx+SEYsCH7JcG3xAT0eJck9yq3y0cvx49bvItLLer8=", + "owner": "input-output-hk", + "repo": "optparse-applicative", + "rev": "7497a29cb998721a9068d5725d49461f2bba0e7a", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "optparse-applicative", + "rev": "7497a29cb998721a9068d5725d49461f2bba0e7a", + "type": "github" + } + }, + "ouroboros-network": { + "flake": false, + "locked": { + "lastModified": 1637082154, + "narHash": "sha256-FNYcUjoy0ZpletEXUIAMbag2Hwb9K3bDRl793NyNy1E=", + "owner": "input-output-hk", + "repo": "ouroboros-network", + "rev": "d613de3d872ec8b4a5da0c98afb443f322dc4dab", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "ouroboros-network", + "rev": "d613de3d872ec8b4a5da0c98afb443f322dc4dab", + "type": "github" + } + }, + "ouroboros-network_2": { + "flake": false, + "locked": { + "lastModified": 1637082154, + "narHash": "sha256-FNYcUjoy0ZpletEXUIAMbag2Hwb9K3bDRl793NyNy1E=", + "owner": "input-output-hk", + "repo": "ouroboros-network", + "rev": "d613de3d872ec8b4a5da0c98afb443f322dc4dab", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "ouroboros-network", + "rev": "d613de3d872ec8b4a5da0c98afb443f322dc4dab", + "type": "github" + } + }, + "plutip": { + "inputs": { + "Win32-network": "Win32-network_2", + "bot-plutus-interface": "bot-plutus-interface", + "cardano-addresses": "cardano-addresses_2", + "cardano-base": "cardano-base_2", + "cardano-config": "cardano-config", + "cardano-crypto": "cardano-crypto_2", + "cardano-ledger": "cardano-ledger_2", + "cardano-node": [ + "cardano-node" + ], + "cardano-prelude": "cardano-prelude_2", + "cardano-wallet": "cardano-wallet_2", + "flake-compat": "flake-compat_2", + "flat": "flat_2", + "goblins": "goblins_2", + "haskell-nix": [ + "haskell-nix" + ], + "iohk-monitoring-framework": "iohk-monitoring-framework_2", + "iohk-nix": "iohk-nix_3", + "nixpkgs": [ + "nixpkgs" + ], + "optparse-applicative": "optparse-applicative_2", + "ouroboros-network": "ouroboros-network_2", + "plutus": "plutus_2", + "plutus-apps": "plutus-apps", + "purescript-bridge": "purescript-bridge", + "servant-purescript": "servant-purescript" + }, + "locked": { + "lastModified": 1645729436, + "narHash": "sha256-fSvMIHXIVQdEO2hVEjI9zqMqwXt+Cw+rvUzmi4FAdSg=", + "owner": "mlabs-haskell", + "repo": "plutip", + "rev": "c2d0ed381cda64bc46dbf68f52cb0d05f76f3a86", + "type": "github" + }, + "original": { + "owner": "mlabs-haskell", + "repo": "plutip", + "rev": "c2d0ed381cda64bc46dbf68f52cb0d05f76f3a86", + "type": "github" + } + }, + "plutus": { + "inputs": { + "cardano-repo-tool": "cardano-repo-tool", + "gitignore-nix": "gitignore-nix", + "hackage-nix": "hackage-nix", + "haskell-language-server": "haskell-language-server", + "haskell-nix": "haskell-nix_3", + "iohk-nix": "iohk-nix_2", + "nixpkgs": "nixpkgs_2", + "pre-commit-hooks-nix": "pre-commit-hooks-nix", + "sphinxcontrib-haddock": "sphinxcontrib-haddock", + "stackage-nix": "stackage-nix" + }, + "locked": { + "lastModified": 1638544940, + "narHash": "sha256-zrlwW4fZPpTRdmvOlT2bWzG9LciNPDQYmp26Jj1SW5s=", + "owner": "input-output-hk", + "repo": "plutus", + "rev": "b778f9abbe172177b579badf83b899ee05fda2e0", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "plutus", + "type": "github" + } + }, + "plutus-apps": { + "flake": false, + "locked": { + "lastModified": 1642502716, + "narHash": "sha256-UULYQppoNjj+EOcV75UT3DOwJF+d609FOYsZZFeAQcM=", + "owner": "input-output-hk", + "repo": "plutus-apps", + "rev": "34fe6eeff441166fee0cd0ceba68c1439f0e93d2", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "plutus-apps", + "rev": "34fe6eeff441166fee0cd0ceba68c1439f0e93d2", + "type": "github" + } + }, + "plutus-apps_2": { + "flake": false, + "locked": { + "lastModified": 1643751916, + "narHash": "sha256-NzvXvydXBfIdym5axI/UaDwe2neF1xK0QiaOnDeYyG0=", + "owner": "t4ccer", + "repo": "plutus-apps", + "rev": "0d3ad307e5875cd5a90df8aa8cea46962eaa5c85", + "type": "github" + }, + "original": { + "owner": "t4ccer", + "repo": "plutus-apps", + "rev": "0d3ad307e5875cd5a90df8aa8cea46962eaa5c85", + "type": "github" + } + }, + "plutus-extra": { + "flake": false, + "locked": { + "lastModified": 1643071526, + "narHash": "sha256-cPp2tgQMRvgl+0XrtkaY4jLYFt7jBQwjPi9z7FIYu+8=", + "owner": "Liqwid-Labs", + "repo": "plutus-extra", + "rev": "4722305495c8c4b03ff06debf0f4a041768a5467", + "type": "github" + }, + "original": { + "owner": "Liqwid-Labs", + "repo": "plutus-extra", + "rev": "4722305495c8c4b03ff06debf0f4a041768a5467", + "type": "github" + } + }, + "plutus-simple-model": { + "flake": false, + "locked": { + "lastModified": 1644261923, + "narHash": "sha256-bs8XhMhh6Kx2jy9zdjW8NfgsWgBu4+iAQriSXXT3bro=", + "owner": "t4ccer", + "repo": "plutus-simple-model", + "rev": "48c186f96e3a8a07bceb1a4b39a7dfeacddde42b", + "type": "github" + }, + "original": { + "owner": "t4ccer", + "repo": "plutus-simple-model", + "rev": "48c186f96e3a8a07bceb1a4b39a7dfeacddde42b", + "type": "github" + } + }, + "plutus-tx-spooky": { + "flake": false, + "locked": { + "lastModified": 1639082449, + "narHash": "sha256-VrUwoB5l1GhmU9g3dafdbvcHERDzeyl78VESYRrUWXY=", + "owner": "fresheyeball", + "repo": "plutus-tx-spooky", + "rev": "0c409907fa5b6aee4a2f2d18f871b850a8547fdf", + "type": "gitlab" + }, + "original": { + "owner": "fresheyeball", + "repo": "plutus-tx-spooky", + "rev": "0c409907fa5b6aee4a2f2d18f871b850a8547fdf", + "type": "gitlab" + } + }, + "plutus_2": { + "flake": false, + "locked": { + "lastModified": 1642090150, + "narHash": "sha256-0l8kWR9R0XkkJInbKP/1l8e5jCVhZQ7fVo7IRaXepQ8=", + "owner": "input-output-hk", + "repo": "plutus", + "rev": "65bad0fd53e432974c3c203b1b1999161b6c2dce", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "plutus", + "rev": "65bad0fd53e432974c3c203b1b1999161b6c2dce", + "type": "github" + } + }, + "plutus_3": { + "flake": false, + "locked": { + "lastModified": 1642090150, + "narHash": "sha256-0l8kWR9R0XkkJInbKP/1l8e5jCVhZQ7fVo7IRaXepQ8=", + "owner": "input-output-hk", + "repo": "plutus", + "rev": "65bad0fd53e432974c3c203b1b1999161b6c2dce", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "plutus", + "rev": "65bad0fd53e432974c3c203b1b1999161b6c2dce", + "type": "github" + } + }, + "pre-commit-hooks-nix": { + "flake": false, + "locked": { + "lastModified": 1624971177, + "narHash": "sha256-Amf/nBj1E77RmbSSmV+hg6YOpR+rddCbbVgo5C7BS0I=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "397f0713d007250a2c7a745e555fa16c5dc8cadb", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "purescript-bridge": { + "flake": false, + "locked": { + "lastModified": 1635433489, + "narHash": "sha256-paaId4GJ9/Z5LstYfakiCJZ2p9Q5NMHXdXUx5rTPQKI=", + "owner": "input-output-hk", + "repo": "purescript-bridge", + "rev": "366fc70b341e2633f3ad0158a577d52e1cd2b138", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "purescript-bridge", + "rev": "366fc70b341e2633f3ad0158a577d52e1cd2b138", + "type": "github" + } + }, + "purescript-bridge_2": { + "flake": false, + "locked": { + "lastModified": 1635433489, + "narHash": "sha256-paaId4GJ9/Z5LstYfakiCJZ2p9Q5NMHXdXUx5rTPQKI=", + "owner": "input-output-hk", + "repo": "purescript-bridge", + "rev": "366fc70b341e2633f3ad0158a577d52e1cd2b138", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "purescript-bridge", + "rev": "366fc70b341e2633f3ad0158a577d52e1cd2b138", + "type": "github" + } + }, + "root": { + "inputs": { + "Win32-network": "Win32-network", + "bot-plutus-interface": [ + "plutip", + "bot-plutus-interface" + ], + "cardano-addresses": "cardano-addresses", + "cardano-base": "cardano-base", + "cardano-crypto": "cardano-crypto", + "cardano-ledger": "cardano-ledger", + "cardano-node": "cardano-node", + "cardano-prelude": "cardano-prelude", + "cardano-wallet": "cardano-wallet", + "flake-compat": "flake-compat", + "flat": "flat", + "goblins": "goblins", + "haskell-nix": "haskell-nix", + "iohk-monitoring-framework": "iohk-monitoring-framework", + "iohk-nix": "iohk-nix", + "nixpkgs": [ + "haskell-nix", + "nixpkgs" + ], + "optparse-applicative": "optparse-applicative", + "ouroboros-network": "ouroboros-network", + "plutip": "plutip", + "plutus": "plutus_3", + "plutus-apps": "plutus-apps_2", + "plutus-extra": "plutus-extra", + "plutus-simple-model": "plutus-simple-model", + "plutus-tx-spooky": "plutus-tx-spooky", + "purescript-bridge": "purescript-bridge_2", + "servant-purescript": "servant-purescript_2" + } + }, + "servant-purescript": { + "flake": false, + "locked": { + "lastModified": 1635969498, + "narHash": "sha256-VkM9Q2XkDEnQh6khptoIjQ9xW7Fc2wsOJ4vPYDzBTD4=", + "owner": "input-output-hk", + "repo": "servant-purescript", + "rev": "ebea59c7bdfc0338d83fca772b9a57e28560bcde", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "servant-purescript", + "rev": "ebea59c7bdfc0338d83fca772b9a57e28560bcde", + "type": "github" + } + }, + "servant-purescript_2": { + "flake": false, + "locked": { + "lastModified": 1635969498, + "narHash": "sha256-VkM9Q2XkDEnQh6khptoIjQ9xW7Fc2wsOJ4vPYDzBTD4=", + "owner": "input-output-hk", + "repo": "servant-purescript", + "rev": "ebea59c7bdfc0338d83fca772b9a57e28560bcde", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "servant-purescript", + "rev": "ebea59c7bdfc0338d83fca772b9a57e28560bcde", + "type": "github" + } + }, + "sphinxcontrib-haddock": { + "flake": false, + "locked": { + "lastModified": 1594136664, + "narHash": "sha256-O9YT3iCUBHP3CEF88VDLLCO2HSP3HqkNA2q2939RnVY=", + "owner": "michaelpj", + "repo": "sphinxcontrib-haddock", + "rev": "f3956b3256962b2d27d5a4e96edb7951acf5de34", + "type": "github" + }, + "original": { + "owner": "michaelpj", + "repo": "sphinxcontrib-haddock", + "type": "github" + } + }, + "stackage": { + "flake": false, + "locked": { + "lastModified": 1631495632, + "narHash": "sha256-jnkmC3PqnRpM4y74b4ZxgD+MTpz3ovxoU99TBCNRDP0=", + "owner": "input-output-hk", + "repo": "stackage.nix", + "rev": "3f43e2e72e0853e911497277ec094933892718a9", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "stackage.nix", + "type": "github" + } + }, + "stackage-nix": { + "flake": false, + "locked": { + "lastModified": 1597712578, + "narHash": "sha256-c/pcfZ6w5Yp//7oC0hErOGVVphBLc5vc4IZlWKZ/t6E=", + "owner": "input-output-hk", + "repo": "stackage.nix", + "rev": "e32c8b06d56954865725514ce0d98d5d1867e43a", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "stackage.nix", + "type": "github" + } + }, + "stackage_2": { + "flake": false, + "locked": { + "lastModified": 1642468901, + "narHash": "sha256-+Hu4m9i8v8Moey/C8fy8juyxB729JdsXz02cK8nJXLk=", + "owner": "input-output-hk", + "repo": "stackage.nix", + "rev": "7544f8fd16bb92b7cf90cb51cb4ddc43173526de", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "stackage.nix", + "type": "github" + } + }, + "stackage_3": { + "flake": false, + "locked": { + "lastModified": 1638580388, + "narHash": "sha256-mD5kmTPmZ56RGqeGo0pqmnrLU7R+uns6+c7UUu8DRtE=", + "owner": "input-output-hk", + "repo": "stackage.nix", + "rev": "ce0a5bb35f8cad47db3a987d76d3f4c82a941986", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "stackage.nix", + "type": "github" + } + }, + "utils": { + "locked": { + "lastModified": 1623875721, + "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/mlabs/flake.nix b/mlabs/flake.nix new file mode 100644 index 000000000..7d5700da3 --- /dev/null +++ b/mlabs/flake.nix @@ -0,0 +1,179 @@ +{ + description = "mlabs-plutus-use-cases"; + + inputs = { + haskell-nix.url = "github:L-as/haskell.nix/ac825b91c202947ec59b1a477003564cc018fcec"; + haskell-nix.inputs.nixpkgs.follows = "haskell-nix/nixpkgs-unstable"; + + nixpkgs.follows = "haskell-nix/nixpkgs"; + + iohk-nix.url = "github:input-output-hk/iohk-nix"; + + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; + + # all inputs below here are for pinning with haskell.nix + cardano-addresses = { + url = + "github:input-output-hk/cardano-addresses/d2f86caa085402a953920c6714a0de6a50b655ec"; + flake = false; + }; + cardano-base = { + url = + "github:input-output-hk/cardano-base/654f5b7c76f7cc57900b4ddc664a82fc3b925fb0"; + flake = false; + }; + cardano-crypto = { + url = + "github:input-output-hk/cardano-crypto/f73079303f663e028288f9f4a9e08bcca39a923e"; + flake = false; + }; + cardano-ledger = { + url = + "github:input-output-hk/cardano-ledger/bf008ce028751cae9fb0b53c3bef20f07c06e333"; + flake = false; + }; + cardano-node = { + url = "github:input-output-hk/cardano-node/4f65fb9a27aa7e3a1873ab4211e412af780a3648"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + cardano-prelude = { + url = + "github:input-output-hk/cardano-prelude/bb4ed71ba8e587f672d06edf9d2e376f4b055555"; + flake = false; + }; + cardano-wallet = { + url = + "github:j-mueller/cardano-wallet/760140e238a5fbca61d1b286d7a80ece058dc729"; + flake = false; + }; + flat = { + url = + "github:input-output-hk/flat/ee59880f47ab835dbd73bea0847dab7869fc20d8"; + flake = false; + }; + goblins = { + url = + "github:input-output-hk/goblins/cde90a2b27f79187ca8310b6549331e59595e7ba"; + flake = false; + }; + iohk-monitoring-framework = { + url = + "github:input-output-hk/iohk-monitoring-framework/46f994e216a1f8b36fe4669b47b2a7011b0e153c"; + flake = false; + }; + optparse-applicative = { + url = + "github:input-output-hk/optparse-applicative/7497a29cb998721a9068d5725d49461f2bba0e7a"; + flake = false; + }; + ouroboros-network = { + url = + "github:input-output-hk/ouroboros-network/d613de3d872ec8b4a5da0c98afb443f322dc4dab"; + flake = false; + }; + plutus = { + url = + "github:input-output-hk/plutus/65bad0fd53e432974c3c203b1b1999161b6c2dce"; + flake = false; + }; + plutus-apps = { + url = + "github:t4ccer/plutus-apps/0d3ad307e5875cd5a90df8aa8cea46962eaa5c85"; + flake = false; + }; + plutus-extra = { + url = + "github:Liqwid-Labs/plutus-extra/4722305495c8c4b03ff06debf0f4a041768a5467"; + flake = false; + }; + plutus-tx-spooky = { + url = + "gitlab:fresheyeball/plutus-tx-spooky/0c409907fa5b6aee4a2f2d18f871b850a8547fdf"; + flake = false; + }; + plutus-simple-model = { + url = + "github:t4ccer/plutus-simple-model/48c186f96e3a8a07bceb1a4b39a7dfeacddde42b"; + flake = false; + }; + purescript-bridge = { + url = + "github:input-output-hk/purescript-bridge/366fc70b341e2633f3ad0158a577d52e1cd2b138"; + flake = false; + }; + servant-purescript = { + url = + "github:input-output-hk/servant-purescript/ebea59c7bdfc0338d83fca772b9a57e28560bcde"; + flake = false; + }; + Win32-network = { + url = + "github:input-output-hk/Win32-network/3825d3abf75f83f406c1f7161883c438dac7277d"; + flake = false; + }; + plutip = { + url = "github:mlabs-haskell/plutip/c2d0ed381cda64bc46dbf68f52cb0d05f76f3a86"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.haskell-nix.follows = "haskell-nix"; + inputs.cardano-node.follows = "cardano-node"; + }; + bot-plutus-interface = { + follows = "plutip/bot-plutus-interface"; + }; + }; + + outputs = { self, nixpkgs, haskell-nix, iohk-nix, ... }@inputs: + let + defaultSystems = [ "x86_64-linux" "x86_64-darwin" ]; + + perSystem = nixpkgs.lib.genAttrs defaultSystems; + + nixpkgsFor = system: + import nixpkgs { + overlays = [ haskell-nix.overlay iohk-nix.overlays.crypto ]; + inherit (haskell-nix) config; + inherit system; + }; + + nixpkgsFor' = system: import nixpkgs { inherit system; }; + + projectFor = system: + let + pkgs = nixpkgsFor system; + plutus = import inputs.plutus { inherit system; }; + src = ./.; + in import ./nix/haskell.nix { inherit src inputs pkgs system; }; + + in { + flake = perSystem (system: (projectFor system).flake { }); + + defaultPackage = perSystem (system: + let lib = "mlabs-plutus-use-cases:lib:mlabs-plutus-use-cases"; + in self.flake.${system}.packages.${lib}); + + packages = perSystem (system: self.flake.${system}.packages); + + apps = perSystem (system: self.flake.${system}.apps); + + devShell = perSystem (system: self.flake.${system}.devShell); + + # This will build all of the project's executables and the tests + check = perSystem (system: + (nixpkgsFor system).runCommand "combined-check" { + nativeBuildInputs = builtins.attrValues self.checks.${system} + ++ builtins.attrValues self.flake.${system}.packages + ++ [ self.flake.${system}.devShell.inputDerivation ]; + } "touch $out"); + + # NOTE `nix flake check` will not work at the moment due to use of + # IFD in haskell.nix + # + # Includes all of the packages in the `checks`, otherwise only the + # test suite would be included + checks = perSystem (system: self.flake.${system}.checks); + hydraJobs.checks.x86_64-linux = self.checks.x86_64-linux; + }; +} diff --git a/mlabs/fourmolu.yaml b/mlabs/fourmolu.yaml new file mode 100644 index 000000000..ed2de01bd --- /dev/null +++ b/mlabs/fourmolu.yaml @@ -0,0 +1,8 @@ +indentation: 2 +comma-style: leading +record-brace-space: true +indent-wheres: true +diff-friendly-import-export: true +respectful: true +haddock-style: multi-line +newlines-between-decls: 1 diff --git a/mlabs/governance-demo/Main.hs b/mlabs/governance-demo/Main.hs new file mode 100644 index 000000000..d4ac7e06b --- /dev/null +++ b/mlabs/governance-demo/Main.hs @@ -0,0 +1,132 @@ +-- | Simulator demo for Governance +module Main ( + main, +) where + +import PlutusTx.Prelude +import Prelude (IO, getLine, show, undefined) + +import Control.Monad (forM, forM_, when) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Data.Aeson (FromJSON, Result (Success), encode, fromJSON) +import Data.Functor (void) +import Data.Monoid (Last (..)) + +import Mlabs.Governance.Contract.Api (Deposit (..), QueryBalance (..), Withdraw (..)) +import Mlabs.Governance.Contract.Simulator.Handler (GovernanceContracts (..)) +import Mlabs.Governance.Contract.Simulator.Handler qualified as Handler +import Mlabs.Governance.Contract.Validation (AssetClassGov (..)) + +import Ledger (CurrencySymbol, PaymentPubKeyHash (unPaymentPubKeyHash), PubKeyHash, TokenName, pubKeyHash, txId) +import Ledger.Constraints (mustPayToPubKey) +import Plutus.V1.Ledger.Value qualified as Value + +import Plutus.PAB.Effects.Contract.Builtin (Builtin) +import Plutus.PAB.Simulator (Simulation) +import Plutus.PAB.Simulator qualified as Simulator +import Plutus.PAB.Webserver.Server qualified as PWS +import Wallet.Emulator.Types (Wallet (..), mockWalletPaymentPubKeyHash) + +import Mlabs.Plutus.PAB (call, waitForLast) +import Mlabs.System.Console.PrettyLogger (logNewLine) +import Mlabs.System.Console.Utils (logAction, logBalance, logMlabs) +import Wallet.Emulator.Wallet (mockWalletAddress) + +-- | Main function to run simulator +main :: IO () +main = void $ + Simulator.runSimulationWith Handler.handlers $ do + Simulator.logString @(Builtin GovernanceContracts) "Starting Governance PAB webserver" + shutdown <- PWS.startServerDebug + let simWallets = Handler.wallets + (wallet1 : wallet2 : wallet3 : _) = simWallets + (cids, gov) <- + subscript + "Initializing contracts\nWallet 1 mints and distributes initial GOV tokens" + simWallets + (itializeContracts wallet1) + let [wCid1, wCid2, wCid3] = cids + + subscript_ + "Wallet 2 deposits 55 GOV (xGOV tokens being minted as result) " + simWallets + $ deposit wCid2 55 + + subscript_ + "Wallet 2 queries amount of GOV deposited" + simWallets + $ getBalance wCid2 wallet2 + + subscript_ + "Wallet 2 deposits 10 more GOV" + simWallets + $ deposit wCid2 10 + + subscript_ + "Wallet 2 queries amount of GOV deposited" + simWallets + $ getBalance wCid2 wallet2 + + subscript_ + "Wallet 2 withdraws 60 GOV" + simWallets + $ withdraw wCid2 wallet2 60 + + subscript_ + "Wallet 2 queries amount of GOV deposited" + simWallets + $ getBalance wCid2 wallet2 + + subscript_ + "Finally, Wallet 3 queries amount of GOV deposited" + simWallets + $ getBalance wCid3 wallet3 + + Simulator.logString @(Builtin GovernanceContracts) "Scripted part is over\nPress Enter to stop and exit" + void $ liftIO getLine + shutdown + where + subscript_ msg wallets simulation = void $ subscript msg wallets simulation + subscript msg wallets simulation = do + logAction msg + next + res <- simulation + Simulator.waitNSlots 1 + mapM_ printBalance wallets + next + return res + + next = do + logNewLine + void $ Simulator.waitNSlots 5 + +-- shortcut for Governance initialization +itializeContracts admin = do + cidInit <- Simulator.activateContract admin Bootstrap + govCs <- waitForLast cidInit + void $ Simulator.waitUntilFinished cidInit + let gov = AssetClassGov govCs Handler.govTokenName + cids <- forM Handler.wallets $ \w -> Simulator.activateContract w (Governance gov) + return (cids, gov) + +-- shortcits for endpoint calls +deposit cid amount = call cid $ Deposit amount + +withdraw cid wallet amount = call cid $ Withdraw [(unPaymentPubKeyHash $ mockWalletPaymentPubKeyHash wallet, amount)] + +getBalance cid wallet = do + call cid $ QueryBalance $ unPaymentPubKeyHash $ mockWalletPaymentPubKeyHash wallet + govBalance :: Integer <- waitForLast cid + logAction $ "Balance is " ++ show govBalance + +printBalance :: Wallet -> Simulation (Builtin schema) () +printBalance wallet = do + v <- Simulator.valueAt $ mockWalletAddress wallet + logBalance ("WALLET " <> show wallet) v + +-- cfg = +-- BootstrapCfg +-- { wallets = Wallet <$> [1 .. 3] -- wallets participating, wallet #1 is admin +-- , govTokenName = "GOVToken" -- name of GOV token to be paid in exchange of xGOV tokens +-- , govAmount = 100 -- GOV amount each wallet gets on start +-- } diff --git a/mlabs/governance-spec.md b/mlabs/governance-spec.md new file mode 100644 index 000000000..379403317 --- /dev/null +++ b/mlabs/governance-spec.md @@ -0,0 +1,80 @@ +# Governance Example Spec + +This will provide some simple governance functions as example behavior to be used across projects and as a simple demonstration of best-practices + +After the initial scaffold, we may adjust the governance contract to perform initiation and update features for contract configuration, perhaps of some custom data, or perhaps using the existing `stablecoin` example from the plutus monorepo. + +We may also move some of the implementations into an open-source library, where possible + +This Contract will deal with a custom, pre minted token called GOV. + +in reality this GOV token can be any token at all, the primary purpose of the contract is to allow users to store and retreive GOV tokens, report on GOV token holdings (as they may be used in a vote), and provide rewards. + +For this contract, the primitive Plutus API, and not the state machine API will be used. + +The xGOV 'token' currently is a family of tokens. In order to enable features such as futures trading we must hold the information to whom the token belongs in the token itself. This is currently dons simply by setting TokenName = PubKeyHash of the one calling Deposit. + +## Governance Contract: + +### Deposit +prerequisites: +user has specified amount of GOV tokens + +input: { amount :: Integer } + +behaviors: + +user must provide `amount` of GOV tokens to contract or else the contract errors out. +`amount` of GOV tokens associated with this user deposit must be reportable and we must be able to query this information knowing only the user's address PubKey + +we should mint xGOV tokens (this may be a configurable behavior in a library function to match the input GOV tokens, xGOV tokens must be returned in order to claim GOV tokens from `Withdraw`) + +user cannot provide negative inputs + +### Withdraw + +prerequisites: +user has specified amount of xGOV tokens in their wallet + +input: { amount :: Value } + +behavior: +transfer `amount` of user-provided xGOV tokens to contract and burn them +transfer `amount` of GOV tokens to the user + +if user does not have provided amount of xGOV, error. + +user cannot provide negative inputs + +### ClaimVote +Prerequisites: +user must have all xGOV tokens in the specified Value + +input: { amount :: Value } + +behaviour: +burn the xGOV given, and mint an equal amount of xGOV that are assigned to the PubKeyHash calling. +think Withdraw follewed by Deposit, but in one transaction. + +### ProvideRewards +Prerequisites: +user must have all the tokens and amounts in the specified Value + +input: PlutusTx.Value + +behaviors: +move all rewards from user wallet, divided as evenly as possible to all stakers of GOV tokens, send these tokens directly to the stakers +error out if there are no GOV tokens staked +return any remainder tokens to the caller (only for smallest possible units) + +(we may create an alternative version where stakers can claim their rewards within the contract, as this is more conducive to futures markets (similar to xGOV tokens). + +### QueryBalance + +input: { address :: PubKey } + +returns { amount :: Integer } + +returns the total number of GOV tokens currently stored under the specified address. (may include multiple deposits, partial or full withdrawals may have occured) + +this is used for determining vote weight in democratic procedures diff --git a/mlabs/hie.yaml b/mlabs/hie.yaml new file mode 100644 index 000000000..b68a5d5c2 --- /dev/null +++ b/mlabs/hie.yaml @@ -0,0 +1,18 @@ +cradle: + cabal: + - path: "./src/Mlabs/" + component: "lib:mlabs-plutus-use-cases" + - path: "./app/" + component: "exe:mlabs-plutus-use-cases" + - path: "./governance-demo/" + component: "exe:governance-demo" + - path: "./lendex-demo/" + component: "exe:lendex-demo" + - path: "./nft-demo/" + component: "exe:nft-demo" + - path: "./nft-marketplace/" + component: "exe:nft-marketplace" + - path: "./deploy-app/" + component: "exe:deploy-app" + - path: "./test" + component: "test:mlabs-plutus-use-cases-tests" diff --git a/mlabs/legacy-nft-endpoint-spec.md b/mlabs/legacy-nft-endpoint-spec.md new file mode 100644 index 000000000..3a79d02a9 --- /dev/null +++ b/mlabs/legacy-nft-endpoint-spec.md @@ -0,0 +1,75 @@ +# NFT Contract Specification + +This project adapts the Ethereum-style approach to NFTs as a digital certificate +of authenticity or ownership, it allows a creator to make a digital asset +representing some artwork, then sell the asset, and for owners of the asset to +be confirmed. + +ownership can only be transferred through the contract, so when the asset is +re-sold, a royalty to the artist can be enforced + +## Author Contract + +### StartParams + +prerequisite: none + +input: +Mlabs.NftStateMachine.Contract.Api.StartParams +(content, share, price) + +behavior: instantiates the 'User' Contract, which represents an asset described +by `content` the author is set to the original owner the entire `StartParams` +will need to be kept as some internal state to be referenced/updated, along with +the author and the current owner + +if the price is Nothing - then the NFT is not for sale and the user must call +`SetPrice` to allow sales. + +## User Contract + +All endpoints on this contract presume that AuthorContract.StartParams has been +called. + +### SetPrice + +Prerequiste: none beyond contract instantiation +must be the current owner + +input: +Mlabs.NftStateMachine.Contract.Api.SetPrice + +behavior: +updates the `price` parameter needed for the `Buy` endpoint + +### Buy + +prerequisite: user must have the necessary ada in their wallet the current +asking price specified by a call to either `StartParams` or `SetPrice` must be a +Just. if it is a Nothing, then the asset is not for sale. + +input: +Mlabs.NftStateMachine.Contract.Api.Buy +(price, newprice) + +behavior: + +if the Buy.price is greater than or equal to the asking price, the user's wallet +will be reduced by Buy.Price Ada (the contract must fail if the user has less +than the specified Buy.price) the funds sent by the caller ('the buyer') are +split such that (`share` * `price` parameter amount) is sent to the author, and +the remainder is sent to the current owner. + +for example, if the author set a share to 1/10, and the buyer paid 100 ada, the +authoer would receive 10 ada and the owner would receive the rest. the owner is +set to the caller if the above is successful the asking price is set to the +Buy.newprice. + +### QueryCurrentOwner + +Returns the address of the current owner. + +### QueryCurrentPrice + +Returns the current `price` parameter so that a potential buyer can purchase the +item. diff --git a/mlabs/lendex-demo/Main.hs b/mlabs/lendex-demo/Main.hs new file mode 100644 index 000000000..0ef73d279 --- /dev/null +++ b/mlabs/lendex-demo/Main.hs @@ -0,0 +1,221 @@ +-- | Console demo for Lendex +module Main ( + main, + initContract, + activateInit, + activateAdmin, + activateUser, + activateOracle, + startParams, + toCoin, +) where + +import Prelude + +import Control.Monad (when) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Data.Functor (void) +import Data.Monoid (Last (..)) + +import Ledger.CardanoWallet (WalletNumber (..)) +import Ledger.Constraints (mustPayToPubKey) +import Ledger.Crypto (PubKeyHash (..)) +import Ledger.Tx (getCardanoTxId) +import Ledger.Value qualified as Value +import Playground.Contract (TokenName, Wallet (..)) +import Plutus.Contract hiding (when) +import Plutus.Contracts.Currency qualified as Currency +import Plutus.PAB.Simulator qualified as Simulator +import Wallet.Emulator.Wallet (fromWalletNumber) +import Wallet.Emulator.Wallet qualified as Wallet + +import Ledger (PaymentPubKeyHash (unPaymentPubKeyHash)) +import Mlabs.Lending.Contract qualified as Contract +import Mlabs.Lending.Contract.Api (StartLendex (..)) +import Mlabs.Lending.Contract.Simulator.Handler qualified as Handler +import Mlabs.Lending.Logic.Types hiding (User (..), Wallet (..)) +import Mlabs.Plutus.PAB (call, printBalance, waitForLast) +import Mlabs.System.Console.PrettyLogger (logNewLine) +import Mlabs.System.Console.Utils (logAction, logMlabs) +import Mlabs.Utils.Wallet (walletFromNumber) +import PlutusTx.Ratio qualified as R + +-- | Console demo for Lendex with simulator +main :: IO () +main = Handler.runSimulator lendexId initContract $ do + cur <- activateInit wAdmin + Simulator.waitNSlots 10 + admin <- activateAdmin wAdmin + oracle <- activateOracle wAdmin + users <- mapM activateUser wallets + + let [user1, user2, user3] = users + [coin1, coin2, coin3] = fmap (toCoin cur) [token1, token2, token3] + + call admin . StartLendex $ startParams cur + next + + logMlabs + test "Init users" (pure ()) + + test + ( unlines + [ "Users deposit funds (100 coins in each currrency)." + , "They receive equal amount of aTokens." + ] + ) + $ do + call user1 $ Contract.Deposit 100 coin1 + call user2 $ Contract.Deposit 100 coin2 + call user3 $ Contract.Deposit 100 coin3 + + test "User 1 borrows 60 Euros" $ do + call user1 $ + Contract.AddCollateral + { addCollateral'asset = coin1 + , addCollateral'amount = 100 + } + call user1 $ Contract.Borrow 60 coin2 (Contract.toInterestRateFlag StableRate) + + test "User 3 withdraws 25 Liras" $ do + call user3 $ Contract.Withdraw 25 coin3 + + test + ( unlines + [ "Rate of Euros becomes high and User1's collateral is not enough." + , "User2 liquidates part of the borrow" + ] + ) + $ do + call oracle $ Contract.SetAssetPrice coin2 (R.fromInteger 2) + call user2 $ + Contract.LiquidationCall + { liquidationCall'collateral = coin1 + , liquidationCall'debtUser = toPubKeyHash w1 + , liquidationCall'debtAsset = coin2 + , liquidationCall'debtToCover = 10 + , liquidationCall'receiveAToken = True + } + + test "User 1 repays 20 coins of the loan" $ do + call user1 $ Contract.Repay 20 coin1 (Contract.toInterestRateFlag StableRate) + + liftIO $ putStrLn "Fin (Press enter to Exit)" + where + next = do + logNewLine + void $ Simulator.waitNSlots 10 + + test msg act = do + void act + void $ Simulator.waitNSlots 1 + logAction msg + mapM_ printBalance wals + next + where + wals = [1, 2, 3] + +initContract :: Handler.InitContract +initContract = do + ownPK <- ownPaymentPubKeyHash + logInfo @String "Start forge" + cur <- + mapError + (Contract.toLendexError . show @Currency.CurrencyError) + (Currency.mintContract ownPK (fmap (,amount) [token1, token2, token3])) + let cs = Currency.currencySymbol cur + tell $ Last (Just cs) + logInfo @String "Forged coins" + giveTo ownPK w1 (toVal cs token1) + giveTo ownPK w2 (toVal cs token2) + giveTo ownPK w3 (toVal cs token3) + logInfo @String "Gave money to users" + where + amount :: Integer + amount = 1000 + + toVal cs tn = Value.singleton cs tn amount + + giveTo ownPK w v = do + let pkh = Wallet.mockWalletPaymentPubKeyHash w + when (pkh /= ownPK) $ do + tx <- submitTx $ mustPayToPubKey pkh v + awaitTxConfirmed $ getCardanoTxId tx + +----------------------------------------------------------------------- +-- activate handlers + +activateInit :: Wallet -> Handler.Sim Value.CurrencySymbol +activateInit wal = do + wid <- Simulator.activateContract wal Handler.Init + cur <- waitForLast wid + void $ Simulator.waitUntilFinished wid + return cur + +activateAdmin :: Wallet -> Handler.Sim ContractInstanceId +activateAdmin wal = Simulator.activateContract wal Handler.Admin + +activateUser :: Wallet -> Handler.Sim ContractInstanceId +activateUser wal = Simulator.activateContract wal Handler.User + +activateOracle :: Wallet -> Handler.Sim ContractInstanceId +activateOracle wal = Simulator.activateContract wal Handler.Oracle + +----------------------------------------------------------------------- +-- constants + +lendexId :: LendexId +lendexId = LendexId "lendex" + +-- | Wallets that are used for testing. +wAdmin, w1, w2, w3 :: Wallet +wAdmin = walletFromNumber 4 +w1 = walletFromNumber 1 +w2 = walletFromNumber 2 +w3 = walletFromNumber 3 + +wallets :: [Wallet] +wallets = [w1, w2, w3] + +token1, token2, token3 :: TokenName +token1 = "Dollar" +token2 = "Euro" +token3 = "Lira" + +{- | Corresponding aTokens. We create aTokens in exchange for to the real coins + on our lending app +-} +aToken1, aToken2, aToken3, aAda :: TokenName +aToken1 = Value.tokenName "aDollar" +aToken2 = Value.tokenName "aEuro" +aToken3 = Value.tokenName "aLira" +aAda = Value.tokenName "aAda" + +startParams :: Value.CurrencySymbol -> StartParams +startParams cur = + StartParams + { sp'coins = + fmap + ( \(coin, aCoin) -> + CoinCfg + { coinCfg'coin = coin + , coinCfg'rate = R.fromInteger 1 + , coinCfg'aToken = aCoin + , coinCfg'interestModel = defaultInterestModel + , coinCfg'liquidationBonus = R.reduce 5 100 + } + ) + [(adaCoin, aAda), (toCoin cur token1, aToken1), (toCoin cur token2, aToken2), (toCoin cur token3, aToken3)] + , sp'initValue = Value.assetClassValue adaCoin 1000 + , sp'admins = [toPubKeyHash wAdmin] + , sp'oracles = [toPubKeyHash wAdmin] + } + +toCoin :: Value.CurrencySymbol -> TokenName -> Coin +toCoin cur tn = Value.AssetClass (cur, tn) + +-------------------------------------------------------------------- +-- utils + +toPubKeyHash :: Wallet -> PubKeyHash +toPubKeyHash = unPaymentPubKeyHash . Wallet.mockWalletPaymentPubKeyHash diff --git a/mlabs/lendex-endpoint-spec.md b/mlabs/lendex-endpoint-spec.md new file mode 100644 index 000000000..8b2d07503 --- /dev/null +++ b/mlabs/lendex-endpoint-spec.md @@ -0,0 +1,289 @@ + +# Lendex Endpoints Specification + +This document describes the endpoint-level bahaviours we want to guarantee for the Lendex Contract + +This will include every current/planned endpoint, and for each Endpoint, a list of: +- input type +- prerequiste actions/endpoint calls (implicit state required for the call to succeed on both wallet and script state, implied previous endpoint calls from the user) +- user role qualifications, if any +- expected effects +- known invariant effects/invalid inputs +- known risks which must be mitigated + + + +## About the Lendex Contract + +The Lendex Contract is a cardano adaptation of the Aave lending protocol. + +In Aave, tokens are used to provide the infrastructure of a savings & loan bank on the blockchain. + +Aave supports multiple currencies, and is mainly used to create Collateralized Debt Positions across currencies, Aave will have individual Lendex contract instances available for each currency. + +A user can deposit the currency tied to a given Lendex Contract Instance, and mint a token called an `aToken` (so for `Ada`, `aAda`, `USD`, `aUSD` etc) + +These `aToken`s can then be supplied as Collateral against a loan (usually in a different currency) + +when interest is paid against the loan, this increases the supply of the underlying token, while no additional aTokens are minted, the result is that aToken value has increased, as the aToken exchange rate corresponds to the ratio of (all aTokens in circulation) : (all underlying tokens in the lendex contract). + +On user roles (Actor types): +Almost all users will be suppliers within the system, being a supplier is _required_ to be a borrower, though many users who act as a supplier will never be borrowers. + +Additionally since LQ will be distributed among suppliers, Many (though not necessarily all) Governor users will also be suppliers. + +Liquidators need not be Suppliers though. + +Attackers will assume other roles, attempt to cause invariant behaviours, or take advantage of system rules such that they profit at the expense of the contract or it's users, we want to prevent these kinds of attacks or make them less profitable + + +## Admin Contract + +### StartParams + +input: Mlabs.Lending.Contract.Api.StartParams + +prerequisite: none + +behaviour: instantiate a User Contract with the input as its initial parameters. + +### AddReserve + +input: Mlabs.Lending.Contract.Api.AddReserve + +Prerequisite: none + +behaviour: + +initiate the user's reserve for the currencies specified in the input. + +### QueryAllLendexes + +returns a list of `LendingPool` data associated with each available lendes + +it should provide the lendex address/instanceid, as well as the `LendingPool` it is currently using as config. + +if no lendexes, it should succeed with an empty list. + +## Oracle Contract + +### SetAssetPrice + +input: Mlabs.Lending.Contract.Api.SetAssetPrice + +prerequisite: none + +behaviour: defines the conversion rate from Ada to a given Underlying token based on input values +(input specifies a `Coin` and a `Ray` (rational)) + +in the future we should adjust the oracle to accept information from a trusted source. + +## User Contract + +All Lendex endpoints rely on the Lendex being initialized through a call to `StartParams` + +### Deposit + +input: +Mlabs.Lending.Contract.Api.Deposit (amount, asset) + +Prerequisite: wallet holds underlying currency for Market + +Expected outcome: +Wallet balance of underlying token reduces by amount of asset (plus fees). +these funds are added to the user's reserve +Wallet balance of aToken increases by x / e, these should be newly minted aTokens. +where + x = amount of atokens specified in request body, rounded down such that the wallet has enough underlying token (if necessary) + e = exchange rate aToken: underlying + +if this transaction deposits Ada into the contract, the stake delegation of the user should be maintained. + +Invariant outcomes: +the user should not be able to call a negative number in the request body at all, and should not be able to burn tokens using the endpoint under any circumstances. + +### Withdraw + +input: +Mlabs.Lending.Contract.Api.Withdraw (amount, asset) + +Prerequisite: The user must hold aTokens for the market in question, making the `Deposit` endpoint necessary + + +Expected outcome: +Wallet Balance of aToken is reduced by x. +these funds are added to the user's reserve +Wallet balance of underlying is increased by (x * e) (less fees) +atokens are burned AND any global state tracking total aTokens in circulation is adjusted. + +if another actor in the system has borrowed the underlying currency and paid interest, than the value of e should increase over time, this should be mechanical so it should be testable +where + x = amount of atokens specified in request body, rounded down such that the wallet has enough aTokens (if necessary) + e = exchange rate aToken: underlying + +if user A calls Mint... and then a user B borrows the underlying for the market and holds the borrow for a sufficient length of time to incur interest before repaying, then repays the loan, then the exchange rate should change, meaning user A will have more funds after calling Redeem than when they called Mint... + +invariant outcomes: +negative numbers used for request body should NEVER result in a mint operation, furthermore, minting of aTokens must never happen on this endpoint. + +`e` must always be greater than or equal to the value of `e` at the time of the mint operation. aToken value never goes down. + +other notes: + + +### SetUserReserveAsCollateral + + +we want to deprecate this in favor of AddCollateral, RemoveCollateral + +input: +Mlabs.Lending.Contract.Api.SetUserReserveAsCollateral +(asset, useAsCollateral, portion) +(asset *must* refer to an aToken) + +prerequisite: user must have a reserve of deposited aTokens in wallet, implying that the user has previously called `Deposit` + +collateral ratios are calculated using each aToken : underlying exchange rate, then the exchange rate between that underlying token and Ada, which is provided by the Oracle - this way the collateral and the borrowed currency can be compared via relative value in Ada. + +behavior: +let `userTokens` equal amount of `asset` in user wallet / provided utxos +let `contractTokens` equal amount of `asset` currently locked as collateral for this user +if useAsCollateral is True, then move (`userTokens` of `asset` * `portion`) from User wallet to contract and lock as collateral +if useAsCollateral is false, then move (`contractTokens` of `asset` * `portion`) from contract to user wallet rounding down to ensure that the user does not go below minimum collateral ratios for this Lendex/User contract + + +`asset` must refer to a supported token for this Lendex + +### AddCollateral + +input: { amount :: Integer, assetClass :: AssetClass } + +prerequisite: +the assetClass is an aToken +the user has `amount` of `assetClass` in their wallet (implies that `Deposit` has been called) + +behavior: + +transfers `amount` of `assetClass` from the user's wallet to the contract, locked as the user's Collateral. + +invariant behaviors/inputs: + +can't supply a negative `amount` + +under no circumstances can we release funds to the user. + + +### RemoveCollateral + +input: { amount :: Integer, assetClass :: AssetClass } + +prerequisite: +the assetClass is an aToken +the user has `amount` of `assetClass` locked in the contract as collateral (implies `Deposit` and `AddCollateral` endpoints) + +behaviors: +let `transferAmount` equal the `amount` in hte input, or the user's total collateral in `assetClass`, whichever is Lower. +1) transfers `transferAmount` of `assetClass` from the contract to the user, if the user has sufficient collateral in that assetClass, + +invariant behaviors: +can't supply a negative `amount` +under no circumstances should this reduce the funds of a user, except for network service fees. + + + + +### Borrow + +Input: +Mlabs.Lending.Contract.Api.Borrow +(amount, asset, rate) + + +Users: Borrowers + +Prerequisite: +the user must have an appropriate amount of aTokens already held in the contract Collateral such that their borrow specifications can be met without putting them below minimum collateral Ratio + +collateral ratios are calculated using each aToken : underlying exchange rate, then the exchange rate between that underlying token and Ada, which is provided by the Oracle - this way the collateral and the borrowed currency can be compared via relative value in Ada. + +expected outcomes: +- interest must be advanced on any outstanding borrows for this user - all calculations must use this amount and not the pre-interest amount. + +- wallet of user balance increases in `amount` of the Lendex's underlying token. +- sufficient state must be maintained such that the user's outstanding borrow can always be known or accurately calculated, including interest. +- if the user has existing borrows against their collateral, this must be considered as well such that the user's maximum borrow amount is never exceeded, interest should be recalculated before this check as well if needed. + +invariant behaviours: +the user should not be able to specify negative amounts to borrow or provide collateral. there should be no input such that this endpoint releases collateral to the user. + +Attack Vectors: + +Market Manipulation +a user may attempt to manipulate the market or tamper with oracle behavior, being able to take out a loan during a price fluctuation (where the value of the underlying Token being borrowed is moved down, or the value of the collateral is move upward) allowing a borrow of more funds than the collateral provided once prices normalize + +This may require mitigation with time-weighted-averaging to the contract's benefit. + +### Repay + +input: +Mlabs.Lending.Contract.Api.Repay +(amount, asset, rate) + +Users: Borrowers + +Prerequisite: +the user must have a loan, and in general will have Collateral to secure the loan up to the collateralRatio for the Lendex + +there are edge cases where we are unable to liquidate or have not yet liquidated the user, and they repay a portion of the loan, saving the loan from liquidation. + +expected outcomes +interest against the loan should be recalculated. +the wallet of the user will decrease by the specified amount from input in the Market's underlying Currency (or round down if the amount available is less than the amount in the input) +the outstanding loan payment will be reduced by the amount paid, less interest applied against the loan +Collateral is not released on this endpoint, even if the borrow amount is totally paid off. + +if repaying Ada, the user's stake delegation should be changed to some default? - if this feature is available in plutus + +invariant outcomes: +can't repay a negative amount but 0 is ok. + +### QuerySupportedCurrency +Returns the name of the underlying, the name of the atoken, and the exchange rate between them. + +### QueryInterestRatePerBlock +returns the current effective interest rate for both Stable and Variable interest rate types, expressed as a Rational + +### QueryCurrentBalance +returns the user's funds currently locked in the current lendex, including both underlying tokens and aTokens of multiple kinds. also returns the user's current borrow amount and advances interest. + +### SwapBorrowRateModel +this is to be deprecated + +### QueryInsolventAccounts + +Return a list of `MLabs.Lending.Contract.Api.LiquidationCall` data that are eligible for liquidation, along with the lendex's liquidation bonus rate. + +to be eligible for liquidation, the total value of a loan must be greater than 80% of the total value of all of the user's collateral. + +### LiquidationCall + +prerequisite: + User has `debtToCover` of `debtAsset` + There is an outstanding loan which greater than 80% of the value of the user's collateral, which must be reflected in the input. + +input: MLabs.Lending.Contract.Api.LiquidationCall +(collateral, debtUser, debtAsset, debtToCover, receiveAToken) + +behavior: + +the user can specify the amount they wish to cover in `debtToCover`, once this is paid by the user, it is reduced from the user's total outstanding debt. + +the user's wallet balance of `debtAsset` is reduced by `debtToCover` +let e be the exchange rate between debtAsset and the Collateral Tokens (keep in mind these are aTokens) +the contract will transfer (`debtToCover` * e * liquidation bonus) worth of collateral to the user's wallet. + +this may all or part of the borrower's outstanding debt and/or collateral + +negative inputs not permitted. + + diff --git a/mlabs/mlabs-plutus-use-cases.cabal b/mlabs/mlabs-plutus-use-cases.cabal new file mode 100644 index 000000000..28653fd5a --- /dev/null +++ b/mlabs/mlabs-plutus-use-cases.cabal @@ -0,0 +1,339 @@ +cabal-version: 2.4 +name: mlabs-plutus-use-cases +version: 0.1.0.0 +license-file: LICENSE +author: mlabs +maintainer: anton@mlabs.gmail +build-type: Simple +extra-source-files: CHANGELOG.md + +common common-imports + build-depends: + , aeson + , ansi-terminal + , base + , binary + , bytestring + , cardano-api + , cardano-ledger-alonzo + , containers + , data-default + , extra + , freer-extras + , freer-simple + , insert-ordered-containers + , lens + , mtl + , openapi3 + , playground-common + , plutus-chain-index + , plutus-chain-index-core + , plutus-contract + , plutus-core + , plutus-extra + , plutus-ledger + , plutus-ledger-api + , plutus-ledger-constraints + , plutus-numeric + , plutus-pab + , plutus-tx + , plutus-tx-plugin + , plutus-tx-spooky + , plutus-use-cases + , pretty-show + , prettyprinter + , purescript-bridge + , row-types + , serialise + , stm + , tasty + , tasty-hunit + , text + +common common-language + default-extensions: + NoImplicitPrelude + BangPatterns + DataKinds + DeriveAnyClass + DeriveFoldable + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + ExplicitForAll + FlexibleContexts + FlexibleInstances + GeneralizedNewtypeDeriving + ImportQualifiedPost + LambdaCase + MonoLocalBinds + MultiParamTypeClasses + NumericUnderscores + OverloadedStrings + QuasiQuotes + RankNTypes + RecordWildCards + ScopedTypeVariables + StandaloneDeriving + TemplateHaskell + TupleSections + TypeApplications + TypeFamilies + TypeOperators + TypeSynonymInstances + +common common-configs + default-language: Haskell2010 + +common common-ghc-options + ghc-options: + -fno-ignore-interface-pragmas -fno-omit-interface-pragmas + -fno-specialize -fno-strictness -fno-warn-orphans -fobject-code + -fplugin-opt PlutusTx.Plugin:defer-errors + +library + import: common-imports + import: common-language + import: common-configs + import: common-ghc-options + ghc-options: + -Wall -Wcompat -Wincomplete-uni-patterns -Wredundant-constraints + -Wmissing-export-lists -Wmissing-deriving-strategies -Werror + + hs-source-dirs: src/ + exposed-modules: + Mlabs.Control.Check + Mlabs.Control.Monad.State + Mlabs.Data.LinkedList + Mlabs.Data.List + Mlabs.Data.Ord + Mlabs.Demo.Contract.Burn + Mlabs.Demo.Contract.Mint + Mlabs.Deploy.Governance + Mlabs.Deploy.Nft + Mlabs.Deploy.Utils + Mlabs.EfficientNFT.Api + Mlabs.EfficientNFT.Lock + Mlabs.EfficientNFT.Contract.Aux + Mlabs.EfficientNFT.Contract.ChangeOwner + Mlabs.EfficientNFT.Contract.Burn + Mlabs.EfficientNFT.Contract.MarketplaceBuy + Mlabs.EfficientNFT.Contract.MarketplaceDeposit + Mlabs.EfficientNFT.Contract.MarketplaceRedeem + Mlabs.EfficientNFT.Contract.MarketplaceSetPrice + Mlabs.EfficientNFT.Contract.Mint + Mlabs.EfficientNFT.Contract.SetPrice + Mlabs.EfficientNFT.Contract.FeeWithdraw + Mlabs.EfficientNFT.Marketplace + Mlabs.EfficientNFT.Dao + Mlabs.EfficientNFT.Token + Mlabs.EfficientNFT.Types + Mlabs.Emulator.App + Mlabs.Emulator.Blockchain + Mlabs.Emulator.Scene + Mlabs.Emulator.Script + Mlabs.Emulator.Types + Mlabs.Governance.Contract.Api + Mlabs.Governance.Contract.Emulator.Client + Mlabs.Governance.Contract.Server + Mlabs.Governance.Contract.Simulator.Handler + Mlabs.Governance.Contract.Validation + Mlabs.Lending.Contract + Mlabs.Lending.Contract.Api + Mlabs.Lending.Contract.Emulator.Client + Mlabs.Lending.Contract.Forge + Mlabs.Lending.Contract.Server + Mlabs.Lending.Contract.Simulator.Handler + Mlabs.Lending.Contract.StateMachine + Mlabs.Lending.Logic.App + Mlabs.Lending.Logic.InterestRate + Mlabs.Lending.Logic.React + Mlabs.Lending.Logic.State + Mlabs.Lending.Logic.Types + Mlabs.NFT.Api + Mlabs.NFT.Contract + Mlabs.NFT.Contract.Aux + Mlabs.NFT.Contract.BidAuction + Mlabs.NFT.Contract.Buy + Mlabs.NFT.Contract.CloseAuction + Mlabs.NFT.Contract.Gov + Mlabs.NFT.Contract.Gov.Aux + Mlabs.NFT.Contract.Gov.Fees + Mlabs.NFT.Contract.Gov.Query + Mlabs.NFT.Contract.Init + Mlabs.NFT.Contract.Mint + Mlabs.NFT.Contract.OpenAuction + Mlabs.NFT.Contract.Query + Mlabs.NFT.Contract.SetPrice + Mlabs.NFT.Governance + Mlabs.NFT.Governance.Types + Mlabs.NFT.Governance.Validation + Mlabs.NFT.PAB.MarketplaceContract + Mlabs.NFT.PAB.Run + Mlabs.NFT.PAB.Simulator + Mlabs.NFT.Spooky + Mlabs.NFT.Types + Mlabs.NFT.Validation + Mlabs.NftStateMachine.Contract + Mlabs.NftStateMachine.Contract.Api + Mlabs.NftStateMachine.Contract.Emulator.Client + Mlabs.NftStateMachine.Contract.Forge + Mlabs.NftStateMachine.Contract.Server + Mlabs.NftStateMachine.Contract.Simulator.Handler + Mlabs.NftStateMachine.Contract.StateMachine + Mlabs.NftStateMachine.Logic.App + Mlabs.NftStateMachine.Logic.React + Mlabs.NftStateMachine.Logic.State + Mlabs.NftStateMachine.Logic.Types + Mlabs.Plutus.Contract + Mlabs.Plutus.Contracts.Currency + Mlabs.Plutus.PAB + Mlabs.System.Console.PrettyLogger + Mlabs.System.Console.Utils + Mlabs.Utils.Wallet + +executable mlabs-plutus-use-cases + import: common-imports + import: common-language + import: common-configs + import: common-ghc-options + main-is: app/Main.hs + build-depends: mlabs-plutus-use-cases + +executable deploy-app + import: common-imports + import: common-language + import: common-configs + import: common-ghc-options + main-is: deploy-app/Main.hs + build-depends: + , cardano-api + , cardano-ledger-alonzo + , mlabs-plutus-use-cases + , serialise + +executable nft-state-machine-demo + import: common-imports + import: common-language + import: common-configs + import: common-ghc-options + main-is: nft-state-machine-demo/Main.hs + build-depends: mlabs-plutus-use-cases + +executable governance-demo + import: common-imports + import: common-language + import: common-configs + import: common-ghc-options + main-is: governance-demo/Main.hs + build-depends: mlabs-plutus-use-cases + +executable lendex-demo + import: common-imports + import: common-language + import: common-configs + import: common-ghc-options + main-is: lendex-demo/Main.hs + build-depends: mlabs-plutus-use-cases + +executable nft-marketplace + import: common-imports + import: common-language + import: common-configs + import: common-ghc-options + main-is: nft-marketplace/Main.hs + build-depends: mlabs-plutus-use-cases + +test-suite mlabs-plutus-use-cases-tests + import: common-imports + import: common-language + import: common-configs + import: common-ghc-options + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + ghc-options: -Wall -threaded -rtsopts + + -- -fplugin=RecordDotPreprocessor + + build-depends: + , base + , bot-plutus-interface + , containers + , data-default + , freer-extras + , freer-simple + , lens + , mlabs-plutus-use-cases + , mtl + , playground-common + , plutus-contract + , plutus-core + , plutus-ledger + , plutus-ledger-api + , plutus-ledger-constraints + , plutus-pab + , plutus-tx + , plutus-tx-plugin + , plutus-tx-spooky + , plutus-simple-model + , plutus-use-cases + , plutip + , pretty-show + , prettyprinter + , QuickCheck + , record-dot-preprocessor + , record-hasfield + , tasty + , tasty-expected-failure + , tasty-hunit + , tasty-plutus + , tasty-quickcheck + , text + + other-modules: + Test.Demo.Contract.Mint + Test.EfficientNFT.Resources + Test.EfficientNFT.Script.TokenBurn + Test.EfficientNFT.Script.TokenChangeOwner + Test.EfficientNFT.Script.TokenChangePrice + Test.EfficientNFT.Script.TokenMint + Test.EfficientNFT.Script.TokenUnstake + Test.EfficientNFT.Script.TokenRestake + Test.EfficientNFT.Script.TokenMarketplaceSetPrice + Test.EfficientNFT.Script.TokenMarketplaceBuy + Test.EfficientNFT.Script.TokenMarketplaceRedeem + Test.EfficientNFT.Script.FeeWithdraw + Test.EfficientNFT.Script.Values + Test.EfficientNFT.Size + Test.EfficientNFT.Trace + Test.EfficientNFT.Quickcheck + Test.EfficientNFT.Plutip + Test.Governance.Contract + Test.Governance.Init + Test.Lending.Contract + Test.Lending.Init + Test.Lending.Logic + Test.Lending.QuickCheck + Test.NFT.Contract + Test.NFT.Init + Test.NFT.QuickCheck + Test.NFT.Script.Auction + Test.NFT.Script.Dealing + Test.NFT.Script.Main + Test.NFT.Script.Minting + Test.NFT.Script.Values + Test.NFT.Size + Test.NFT.Trace + Test.NftStateMachine.Contract + Test.NftStateMachine.Init + Test.NftStateMachine.Logic + Test.Utils + + default-extensions: + OverloadedStrings + QuasiQuotes + RecordWildCards + TupleSections diff --git a/mlabs/nft-endpoint-spec.md b/mlabs/nft-endpoint-spec.md new file mode 100644 index 000000000..51ae02fb9 --- /dev/null +++ b/mlabs/nft-endpoint-spec.md @@ -0,0 +1,138 @@ +# Endpoints Documentation + +This document describes endpoints available in NFT markterplace application. + +## Admin endpoints + +### App Init (`app-init`) + +prerequisite: +- none + +input: none + +behaviour: Starts NFT marketplace application, mintes HEAD and unique token. + +## User endpoints + +### Mint (`mint`) + +prerequisite: +- App is initialised +- NFT was not minted before + +input: Mlabs.NFT.Types.MintParams + +behaviour: Mints new NFT. If the price is `Nothing` then the NFT is not for sale +and the owner must call `set-price` to allow sales. + +### Set price (`set-price`) + +prerequisite: +- App is initialised +- User must be current owner +- NFT is not on auction + +input: Mlabs.NFT.Types.SetPriceParams + +behaviour: updates the `info'price` parameter + +### Buy (`buy`) + +prerequisite: +- App is initialised +- User must have necessary ADA in wallet +- `info'price` parameter is not `Nothing` + +input: Mlabs.NFT.Types.BuyRequestUser + +behaviour: + +If the `BuyRequestUser.ur'price` is greater than or equal to the asking price, +the user's wallet will be reduced by Buy.Price ADA (the contract must fail if +the user has less than the specified Buy.price) the funds sent by the caller +('the buyer') are split such that (`share` * `price` parameter amount) is sent +to the author, and the remainder is sent to the current owner. + +For example, if the author set a share to 1/10, and the buyer paid 100 ADA, the +author would receive 10 ADA and the owner would receive the rest. The owner is +set to the caller if the above is successful the asking price is set to the +`BuyRequestUser.ur'newPrice`. + +### Auction open (`auction-open`) + +prerequisite: +- App is initialised +- User must be current owner +- NFT is not on auction +- `as'minBid` is greater or equal to 2 ADA + +input: Mlabs.NFT.Types.AuctionOpenParams + +behaviour: + +Sets the `info'price` parameter to `Nothing` (NFT is no longer for sale), and sets `info'auctionState` to `Just` starting an auction. + +### Auction bid (`auction-bid`) + +prerequisite: +- App is initialised +- NFT is on auction +- Bid (`bp'bidAmount`) is higher than `as'minBid` +- Bid is higher than `as'highesBid`, when `as'highesBid` is `Just` +- `as'deadline` is not reached + +input: Mlabs.NFT.Types.AuctionBidParams + +behaviour: +Bid amount is lock in the script, previous bid is sent back, updates `as'highesBid` + +### Auction close (`auction-close`) + +prerequisite: +- App is initialised +- NFT is on auction +- User is auction winner + +input: Mlabs.NFT.Types.AuctionCloseParams + +behaviour: NFT is sent to user, Highest bid is unlocked from script and paid to +previous owner and author, as described in `buy` endpoint. + +## Query endpoints + +### Query Current Price (`query-current-owner`) + +prerequisite: +- App is initialised + +input: Mlabs.NFT.Types.NftId + +behaviour: Returns current price of NFT. + +### Query Current Owner (`query-current-price`) + +prerequisite: +- App is initialised + +input: Mlabs.NFT.Types.NftId + +behaviour: Returns current owner of NFT. + +### Query List Nfts (`query-list-nfts`) + +prerequisite: +- App is initialised + +input: None + +behaviour: Returns list of all NFTs available in the app. + +### Query Content (`query-content`) + +prerequisite: +- App is initialised + +input: Mlabs.NFT.Types.Content + +behaviour: Returns status of NFT given content. diff --git a/mlabs/nft-marketplace/Main.hs b/mlabs/nft-marketplace/Main.hs new file mode 100644 index 000000000..cd9a2d58b --- /dev/null +++ b/mlabs/nft-marketplace/Main.hs @@ -0,0 +1,7 @@ +module Main (main) where + +import Mlabs.NFT.PAB.Run (runNftMarketplace) +import Prelude + +main :: IO () +main = runNftMarketplace diff --git a/mlabs/nft-marketplace/README.md b/mlabs/nft-marketplace/README.md new file mode 100644 index 000000000..d5003fd9b --- /dev/null +++ b/mlabs/nft-marketplace/README.md @@ -0,0 +1,59 @@ +# NFT marketplace PAB + +This executable runs PAB with NFT marketplace contracts. + +To serve contract PAB requires: + - Cardano node (socket path must be set through config, see `plutus-pab.yaml.sample`) + - cardano-wallet (url must be set through config, see `plutus-pab.yaml.sample`) + - chain-index (url must be set through config, see `plutus-pab.yaml.sample`) + +To be able to sign transactions `--passphrase` need to be provided at PAB launch (see `cabal exec nft-marketplace -- --help`). That passphrase should be for wallet from cardano-wallet (WBE) which `id` will be used for contract activation. + +PAB launch example: +``` +cabal exec nft-marketplace -- --config path/to/plutus-pab.yaml --passphrase walletA_passphrase migrate (creates database) + +cabal exec nft-marketplace -- --config path/to/plutus-pab.yaml --passphrase walletA_passphrase webserver +``` + +To activate NFT admin contract: +``` +curl --location --request POST 'localhost:9080/api/contract/activate' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "caID": { + "tag": "NftAdminContract" + }, + "caWallet": { + "getWalletId": "walletA_id" + } +}' +``` + +To activate NFT user contract: +``` +curl --location --request POST 'localhost:9080/api/contract/activate' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "caID": { + "tag": "UserContract", + "contents": { + "app'\''symbol": { + "unCurrencySymbol": "6666" + } + } + }, + "caWallet": { + "getWalletId": "walletA_id" + } +}' +``` +! Note - this data from the example above: +``` +"contents": { + "app'\''symbol": { + "unCurrencySymbol": "6666" + } + } +``` +provided only as example, real `NftAppSymbol` data should be obtained from the state of admin contract. \ No newline at end of file diff --git a/mlabs/nft-marketplace/plutus-pab.yaml.sample b/mlabs/nft-marketplace/plutus-pab.yaml.sample new file mode 100644 index 000000000..b119e73f4 --- /dev/null +++ b/mlabs/nft-marketplace/plutus-pab.yaml.sample @@ -0,0 +1,55 @@ +dbConfig: + dbConfigFile: pab-core.db + dbConfigPoolSize: 20 + +pabWebserverConfig: + baseUrl: http://localhost:9080 + staticDir: nft-marketplace/dist + permissiveCorsPolicy: False + # Optional timeout (in seconds) for calls to endpoints that are not currently + # available. If this is not set, calls to unavailable endpoints fail + # immediately. + endpointTimeout: 5 + +walletServerConfig: + baseUrl: http://localhost:9081 + wallet: + getWallet: 1 + +nodeServerConfig: + mscBaseUrl: http://localhost:9082 + mscSocketPath: ./node-server.sock + mscKeptBlocks: 100 + mscNetworkId: "8" # Testnet network ID (main net = empty string) + mscSlotConfig: + scSlotZeroTime: 1591566291000 # Wednesday, July 29, 2020 21:44:51 - shelley launch time in milliseconds + scSlotLength: 1000 # In milliseconds + mscFeeConfig: + fcConstantFee: + getLovelace: 10 # Constant fee per transaction in lovelace + fcScriptsFeeFactor: 1.0 # Factor by which to multiply size-dependent scripts fee in lovelace + mscInitialTxWallets: + - getWallet: 1 + - getWallet: 2 + - getWallet: 3 + mscNodeMode: AlonzoNode + +chainIndexConfig: + ciBaseUrl: http://localhost:9083 + ciWatchedAddresses: [] + +requestProcessingConfig: + requestProcessingInterval: 1 + +signingProcessConfig: + spBaseUrl: http://localhost:9084 + spWallet: + getWallet: 1 + +metadataServerConfig: + mdBaseUrl: http://localhost:9085 + +# Optional EKG Server Config +# ---- +# monitoringConfig: +# monitoringPort: 9090 diff --git a/mlabs/nft-state-machine-demo/Main.hs b/mlabs/nft-state-machine-demo/Main.hs new file mode 100644 index 000000000..74c41602c --- /dev/null +++ b/mlabs/nft-state-machine-demo/Main.hs @@ -0,0 +1,110 @@ +-- | Simulator demo for NFTs +module Main ( + main, + activateStartNft, + activateUser, + nftContent, + startParams, +) where + +import Prelude + +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Data.Functor (void) +import Ledger.CardanoWallet (WalletNumber (..)) +import Playground.Contract (Wallet (Wallet)) +import Plutus.Contract (ContractInstanceId) +import Plutus.PAB.Simulator qualified as Simulator +import PlutusTx.Prelude (BuiltinByteString) +import Wallet.Emulator.Wallet (fromWalletNumber) + +import Mlabs.NftStateMachine.Contract qualified as Nft +import Mlabs.NftStateMachine.Contract.Simulator.Handler qualified as Handler +import Mlabs.NftStateMachine.Logic.Types (NftId) +import Mlabs.Plutus.PAB (call, printBalance, waitForLast) +import Mlabs.System.Console.PrettyLogger (logNewLine) +import Mlabs.System.Console.Utils (logAction, logMlabs) +import Mlabs.Utils.Wallet (walletFromNumber) +import PlutusTx.Ratio qualified as R + +-- | Main function to run simulator +main :: IO () +main = Handler.runSimulator startParams $ do + let users = [1, 2, 3] + logMlabs + test "Init users" users (pure ()) + + test "User 1 creates the Mona lisa (NFT)" users (pure ()) + + nid <- activateStartNft user1 + cids <- mapM (activateUser nid) [user1, user2, user3] + let [u1, u2, u3] = cids + + test "User 1 sets the Mona Lisa's price to 100 Lovelace, User 2 buys The Mona Lisa from User 1 for 100 Lovelace (what a deal!), User 2 has specified that the Mona Lisa is not for sale" [1, 2] $ do + setPrice u1 (Just 100) + buy u2 100 Nothing + + test "User 2 sets the sale price to 500 Lovelace, User 3 buys The Mona Lisa from User 2 for 500 Lovelace setting the new sale price to 1000 Lovelace, User 1 receives a royalty from the sale" [1, 2, 3] $ do + setPrice u2 (Just 500) + buy u3 500 (Just 1000) + + liftIO $ putStrLn "Fin (Press enter to Exit)" + where + test msg wals act = do + void act + logAction msg + mapM_ printBalance wals + next + + next = do + logNewLine + void $ Simulator.waitNSlots 10 + +------------------------------------------------------------------------ +-- handlers + +-- | Instanciates start NFT endpoint in the simulator to the given wallet +activateStartNft :: Wallet -> Handler.Sim NftId +activateStartNft wal = do + wid <- Simulator.activateContract wal Handler.StartNft + nftId <- waitForLast wid + void $ Simulator.waitUntilFinished wid + pure nftId + +-- | Instanciates user actions endpoint in the simulator to the given wallet +activateUser :: NftId -> Wallet -> Handler.Sim ContractInstanceId +activateUser nid wal = do + Simulator.activateContract wal $ Handler.User nid + +------------------------------------------------------------- +-- Script helpers + +-- | Call buy NFT endpoint +buy :: ContractInstanceId -> Integer -> Maybe Integer -> Handler.Sim () +buy cid price newPrice = call cid (Nft.Buy price newPrice) + +-- | Call set price for NFT endpoint +setPrice :: ContractInstanceId -> Maybe Integer -> Handler.Sim () +setPrice cid newPrice = call cid (Nft.SetPrice newPrice) + +------------------------------------------------------------- +-- constants + +-- Users for testing +user1, user2, user3 :: Wallet +user1 = walletFromNumber 1 +user2 = walletFromNumber 2 +user3 = walletFromNumber 3 + +-- | Content of NFT +nftContent :: BuiltinByteString +nftContent = "Mona Lisa" + +-- | NFT initial parameters +startParams :: Nft.StartParams +startParams = + Nft.StartParams + { sp'content = nftContent + , sp'share = R.reduce 1 10 + , sp'price = Nothing + } diff --git a/mlabs/nix/README.md b/mlabs/nix/README.md new file mode 100644 index 000000000..72657e986 --- /dev/null +++ b/mlabs/nix/README.md @@ -0,0 +1,65 @@ +# Nix tools for Mlabs Plutus Use Cases + +This directory contains all of the nix helper functions used to build our +dependencies. + +# Formatting + +Use nixfmt (provided by the shell) to format the nix sources. + +# Pinning git dependencies + +Git dependencies are pinned in the `inputs` of the flake. Make sure to set `flake = false;` when adding a new dependencies. When upgrading an existing dependency, replace the commit hash in its `url`. + +# Using flakes commands + +This repository recently switched to flakes, a fairly new (and still experimental) +feature of the nix package manager. Flakes bring a lot of improvements to nix, +including evaluation caching and greater consistency and reproducibility. + +This section is intended to help you transition to using flakes, illustrating flake +equivalents of old `nix-*` commands. + +**Note**: You will need Nix version 2.4 or greater to use the new `nix` command +and subcommands + +**Note**: Due to the use of IFD ("import from derivation") in haskell.nix, `nix flake show` +and `nix flake check` do not currently work. + +## `nix-shell` + +Use `nix develop` + +## `nix-build` + +Use `nix build` + +### Building project components + +Previously, to build specific project components, `nix-build -A mlabs-plutus-use-cases.components.*` +could be used. Project components can now be identified using the flake selector `#` followed by +cabal-like syntax. + +For example, to build the executable `lendex-demo`: + +Old: + +`nix-build -A mlabs-plutus-use-cases.components.exes.lendex-demo` + +New: + +`nix build .#mlabs-plutus-use-cases:exe:deploy-app` + +### Build all derivations that will be built in CI + +`nix build .#check.` builds all of the project packages and runs the tests. + +## More helpful commands + +See all of the flake outputs: `nix flake show` +See flake metadata, including inputs: `nix flake metadata` + +## Compatibility + +In case you cannot upgrade to Nix 2.4 or prefer the older interface, compatibility `shell.nix` and +`default.nix` remain in this repository. diff --git a/mlabs/nix/haskell.nix b/mlabs/nix/haskell.nix new file mode 100644 index 000000000..69698124a --- /dev/null +++ b/mlabs/nix/haskell.nix @@ -0,0 +1,326 @@ +{ src, inputs, pkgs, system, doCoverage ? false, deferPluginErrors ? true, ... }: + +pkgs.haskell-nix.cabalProject { + inherit src; + + name = "mlabs-plutus-use-cases"; + + compiler-nix-name = "ghc8107"; + + shell = { + inputsFrom = [ pkgs.libsodium-vrf ]; + + # Make sure to keep this list updated after upgrading git dependencies! + additional = ps: + with ps; [ + bot-plutus-interface + filemanip + ieee + plutus-extra + tasty-plutus + plutus-pretty + plutus-laws + plutus-numeric + base-deriving-via + cardano-addresses + cardano-addresses-cli + cardano-binary + cardano-crypto + cardano-crypto-class + cardano-crypto-praos + cardano-crypto-wrapper + cardano-ledger-alonzo + cardano-ledger-byron + cardano-ledger-core + cardano-ledger-pretty + cardano-ledger-shelley + cardano-ledger-shelley-ma + cardano-prelude + cardano-slotting + flat + freer-extras + goblins + measures + orphans-deriving-via + playground-common + plutus-chain-index + plutus-ledger-constraints + plutus-contract + plutus-core + plutus-ledger + plutus-ledger-api + plutus-pab + plutus-playground-server + plutus-tx + plutus-tx-plugin + plutus-tx-spooky + plutus-simple-model + plutus-use-cases + plutip + prettyprinter-configurable + quickcheck-dynamic + Win32-network + word-array + ]; + + withHoogle = true; + + tools = { + cabal = "latest"; + haskell-language-server = "latest"; + }; + + exactDeps = true; + + nativeBuildInputs = with pkgs; + [ + # Haskell Tools + haskellPackages.fourmolu + hlint + entr + ghcid + git + + # hls doesn't support preprocessors yet so this has to exist in PATH + haskellPackages.record-dot-preprocessor + + # Graphviz Diagrams for documentation + graphviz + pkg-config + libsodium-vrf + ] ++ (lib.optionals (!stdenv.isDarwin) [ + rPackages.plotly + R + systemdMinimal + ]); + }; + + modules = [{ + packages = { + eventful-sql-common.doHaddock = false; + eventful-sql-common.ghcOptions = ['' + -XDerivingStrategies -XStandaloneDeriving -XUndecidableInstances + -XDataKinds -XFlexibleInstances -XMultiParamTypeClasses'']; + + plutus-use-cases.doHaddock = deferPluginErrors; + plutus-use-cases.flags.defer-plugin-errors = deferPluginErrors; + + plutus-contract.doHaddock = deferPluginErrors; + plutus-contract.flags.defer-plugin-errors = deferPluginErrors; + + plutus-ledger.doHaddock = deferPluginErrors; + plutus-ledger.flags.defer-plugin-errors = deferPluginErrors; + + plutus-simple-model.doHaddock = false; + plutus-simple-model.flags.defer-plugin-errors = deferPluginErrors; + + # see https://github.com/input-output-hk/haskell.nix/issues/1128 + ieee.components.library.libs = pkgs.lib.mkForce [ ]; + + cardano-crypto-praos.components.library.pkgconfig = + pkgs.lib.mkForce [ [ pkgs.libsodium-vrf ] ]; + cardano-crypto-class.components.library.pkgconfig = + pkgs.lib.mkForce [ [ pkgs.libsodium-vrf ] ]; + cardano-wallet-core.components.library.build-tools = + [ pkgs.buildPackages.buildPackages.gitMinimal ]; + cardano-config.components.library.build-tools = + [ pkgs.buildPackages.buildPackages.gitMinimal ]; + + mlabs-plutus-use-cases.components.tests."mlabs-plutus-use-cases-tests".build-tools = + [ inputs.cardano-node.packages.${system}.cardano-node + inputs.cardano-node.packages.${system}.cardano-cli + ]; + + }; + }]; + + extraSources = [ + { + src = inputs.cardano-addresses; + subdirs = [ "core" "command-line" ]; + } + { + src = inputs.cardano-base; + subdirs = [ + "base-deriving-via" + "binary" + "binary/test" + "cardano-crypto-class" + "cardano-crypto-praos" + "cardano-crypto-tests" + "measures" + "orphans-deriving-via" + "slotting" + "strict-containers" + ]; + } + { + src = inputs.cardano-crypto; + subdirs = [ "." ]; + } + { + src = inputs.cardano-ledger; + subdirs = [ + "byron/ledger/impl" + "cardano-ledger-core" + "cardano-protocol-tpraos" + "eras/alonzo/impl" + "eras/byron/chain/executable-spec" + "eras/byron/crypto" + "eras/byron/crypto/test" + "eras/byron/ledger/executable-spec" + "eras/byron/ledger/impl/test" + "eras/shelley/impl" + "eras/shelley-ma/impl" + "eras/shelley/chain-and-ledger/executable-spec" + "eras/shelley/test-suite" + "shelley/chain-and-ledger/shelley-spec-ledger-test" + "libs/non-integral" + "libs/small-steps" + "libs/cardano-ledger-pretty" + "semantics/small-steps-test" + ]; + } + { + src = inputs.cardano-node; + subdirs = [ "cardano-api" ]; + } + { + src = inputs.cardano-prelude; + subdirs = [ "cardano-prelude" "cardano-prelude-test" ]; + } + { + src = inputs.cardano-wallet; + subdirs = [ + "lib/dbvar" + "lib/text-class" + "lib/strict-non-empty-containers" + "lib/core" + "lib/test-utils" + "lib/numeric" + "lib/launcher" + "lib/core-integration" + "lib/cli" + "lib/shelley" + ]; + } + { + src = inputs.flat; + subdirs = [ "." ]; + } + { + src = inputs.goblins; + subdirs = [ "." ]; + } + { + src = inputs.iohk-monitoring-framework; + subdirs = [ + "iohk-monitoring" + "tracer-transformers" + "contra-tracer" + "plugins/backend-aggregation" + "plugins/backend-ekg" + "plugins/backend-monitoring" + "plugins/backend-trace-forwarder" + "plugins/scribe-systemd" + ]; + } + { + src = inputs.optparse-applicative; + subdirs = [ "." ]; + } + { + src = inputs.ouroboros-network; + subdirs = [ + "monoidal-synchronisation" + "typed-protocols" + "typed-protocols-cborg" + "typed-protocols-examples" + "ouroboros-network" + "ouroboros-network-testing" + "ouroboros-network-framework" + "ouroboros-consensus" + "ouroboros-consensus-byron" + "ouroboros-consensus-cardano" + "ouroboros-consensus-shelley" + "io-sim" + "io-classes" + "network-mux" + "ntp-client" + ]; + } + { + src = inputs.plutus; + subdirs = [ + "plutus-core" + "plutus-ledger-api" + "plutus-tx" + "plutus-tx-plugin" + "word-array" + "prettyprinter-configurable" + "stubs/plutus-ghc-stub" + ]; + } + { + src = inputs.plutus-apps; + subdirs = [ + "doc" + "freer-extras" + "playground-common" + "plutus-chain-index" + "plutus-chain-index-core" + "plutus-contract" + "plutus-ledger-constraints" + "plutus-ledger" + "plutus-pab" + "plutus-playground-server" + "plutus-use-cases" + "quickcheck-dynamic" + "web-ghc" + ]; + } + { + src = inputs.plutus-extra; + subdirs = [ + "plutus-extra" + "tasty-plutus" + "plutus-pretty" + "plutus-numeric" + "plutus-golden" + "plutus-laws" + "plutus-list" + "plutus-size-check" + "quickcheck-plutus-instances" + "plutus-deriving" + ]; + } + { + src = inputs.plutus-tx-spooky; + subdirs = [ "." ]; + } + { + src = inputs.plutus-simple-model; + subdirs = [ "." ]; + } + { + src = inputs.purescript-bridge; + subdirs = [ "." ]; + } + { + src = inputs.servant-purescript; + subdirs = [ "." ]; + } + { + src = inputs.Win32-network; + subdirs = [ "." ]; + } + { + src = inputs.plutip; + subdirs = [ "." ]; + } + { + src = inputs.bot-plutus-interface; + subdirs = [ "." ]; + } + ]; +} diff --git a/mlabs/shell.nix b/mlabs/shell.nix new file mode 100644 index 000000000..0d904689f --- /dev/null +++ b/mlabs/shell.nix @@ -0,0 +1,13 @@ +( + import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) { + src = ./.; + } +).shellNix.default diff --git a/mlabs/src/Mlabs/Control/Check.hs b/mlabs/src/Mlabs/Control/Check.hs new file mode 100644 index 000000000..00274990c --- /dev/null +++ b/mlabs/src/Mlabs/Control/Check.hs @@ -0,0 +1,51 @@ +-- | Common input check functions +module Mlabs.Control.Check ( + isNonNegative, + isPositive, + isPositiveRational, + isUnitRange, +) where + +import Control.Monad.Except (MonadError (..)) +import PlutusTx.Prelude +import PlutusTx.Ratio qualified as R + +{-# INLINEABLE isNonNegative #-} +isNonNegative :: + (Applicative m, MonadError BuiltinByteString m) => + BuiltinByteString -> + Integer -> + m () +isNonNegative msg val + | val >= 0 = pure () + | otherwise = throwError $ msg <> " should be non-negative" + +{-# INLINEABLE isPositive #-} +isPositive :: + (Applicative m, MonadError BuiltinByteString m) => + BuiltinByteString -> + Integer -> + m () +isPositive msg val + | val > 0 = pure () + | otherwise = throwError $ msg <> " should be positive" + +{-# INLINEABLE isPositiveRational #-} +isPositiveRational :: + (Applicative m, MonadError BuiltinByteString m) => + BuiltinByteString -> + Rational -> + m () +isPositiveRational msg val + | val > R.fromInteger 0 = pure () + | otherwise = throwError $ msg <> " should be positive" + +{-# INLINEABLE isUnitRange #-} +isUnitRange :: + (Applicative m, MonadError BuiltinByteString m) => + BuiltinByteString -> + Rational -> + m () +isUnitRange msg val + | val >= R.fromInteger 0 && val <= R.fromInteger 1 = pure () + | otherwise = throwError $ msg <> " should have unit range [0, 1]" diff --git a/mlabs/src/Mlabs/Control/Monad/State.hs b/mlabs/src/Mlabs/Control/Monad/State.hs new file mode 100644 index 000000000..0d8d40b86 --- /dev/null +++ b/mlabs/src/Mlabs/Control/Monad/State.hs @@ -0,0 +1,54 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +-- | Common plutus instances for StateT +module Mlabs.Control.Monad.State ( + PlutusState, + MonadError (..), + MonadState (..), + runStateT, + gets, + guardError, +) where + +import PlutusTx.Prelude + +import Control.Monad.Except (MonadError (..)) +import Control.Monad.State.Strict (MonadState (..), StateT (..), gets) + +-- | State update of plutus contracts +type PlutusState st = StateT st (Either BuiltinByteString) + +instance Functor (PlutusState st) where + {-# INLINEABLE fmap #-} + fmap f (StateT a) = StateT $ fmap g . a + where + g (v, st) = (f v, st) + +instance Applicative (PlutusState st) where + {-# INLINEABLE pure #-} + pure a = StateT (\st -> Right (a, st)) + + {-# INLINEABLE (<*>) #-} + (StateT f) <*> (StateT a) = StateT $ \st -> case f st of + Left err -> Left err + Right (f1, st1) -> do + (a1, st2) <- a st1 + return (f1 a1, st2) + +------------------------------------------------ + +{-# INLINEABLE guardError #-} + +{- | Execute further if condition is True or throw error with + given error message. +-} +guardError :: + ( Applicative m + , MonadError BuiltinByteString m + ) => + BuiltinByteString -> + Bool -> + m () +guardError msg isTrue + | isTrue = pure () + | otherwise = throwError msg diff --git a/mlabs/src/Mlabs/Data/LinkedList.hs b/mlabs/src/Mlabs/Data/LinkedList.hs new file mode 100644 index 000000000..c65d8b33c --- /dev/null +++ b/mlabs/src/Mlabs/Data/LinkedList.hs @@ -0,0 +1,108 @@ +{-# LANGUAGE UndecidableInstances #-} + +module Mlabs.Data.LinkedList ( + LList (..), + LowBounded (..), + nextNode, + getNodeKey, + canInsertAfter, + pointNodeTo, + pointNodeToMaybe, + head'info, + head'next, + node'key, + node'info, + node'next, + _HeadLList, + _NodeLList, +) where + +import Control.Lens +import Data.Aeson (FromJSON, ToJSON) +import GHC.Generics (Generic) +import PlutusTx qualified +import PlutusTx.Prelude +import Prelude qualified as Hask + +-- | Class of Types that have a lower bound. +class LowBounded a where + lowBound :: a + +data LList key headInfo nodeInfo + = HeadLList + { _head'info :: headInfo + , _head'next :: Maybe key + } + | NodeLList + { _node'key :: key + , _node'info :: nodeInfo + , _node'next :: Maybe key + } + deriving stock (Hask.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.unstableMakeIsData ''LList +PlutusTx.makeLift ''LList +makeLenses ''LList +makePrisms ''LList + +instance (Eq k, Eq h, Eq n) => Eq (LList k h n) where + {-# INLINEABLE (==) #-} + (HeadLList a b) == (HeadLList a' b') = a == a' && b == b' + (NodeLList a b c) == (NodeLList a' b' c') = a == a' && b == b' && c == c' + _ == _ = False + +instance (Eq (LList key headInfo nodeInfo), Ord key) => Ord (LList key headInfo nodeInfo) where + {-# INLINEABLE (<=) #-} + HeadLList {} <= NodeLList {} = True + (NodeLList k1 _ _) <= (NodeLList k2 _ _) = k1 <= k2 + _ <= _ = False + +instance Eq key => Hask.Eq (LList key headInfo nodeInfo) where + (NodeLList k1 _ _) == (NodeLList k2 _ _) = k1 == k2 + _ == _ = False + +instance (Hask.Eq (LList key headInfo nodeInfo), Hask.Ord key) => Hask.Ord (LList key headInfo nodeInfo) where + HeadLList {} <= NodeLList {} = True + (NodeLList k1 _ _) <= (NodeLList k2 _ _) = k1 Hask.<= k2 + _ <= _ = False + +nextNode :: forall a b c. LList a b c -> Maybe a +nextNode = \case + HeadLList _ n -> n + NodeLList _ _ n -> n + +-- | Node Key getter. +getNodeKey :: LowBounded a => forall b c. LList a b c -> a +getNodeKey = \case + HeadLList _ _ -> lowBound + NodeLList k _ _ -> k + +{- | Utility function that checks if a node can be inserted after another list + node. +-} +canInsertAfter :: (LowBounded a, Ord a) => forall b c. LList a b c -> LList a b c -> Bool +insertingNode `canInsertAfter` leftNode = insKey > leftKey && nextOk + where + insKey = getNodeKey insertingNode + leftKey = getNodeKey leftNode + nextOk = case nextNode leftNode of + Nothing -> True + Just k -> insKey < k + +-- | Utility function that returns the updated List Node. +pointNodeTo :: (Ord a) => forall b c. LList a b c -> LList a b c -> Maybe (LList a b c) +pointNodeTo a b = pointNodeToMaybe a (Just b) + +{- | Utility function that optionally points List Node to another List node, + and returns the updated List Node. +-} +pointNodeToMaybe :: (Ord a) => forall b c. LList a b c -> Maybe (LList a b c) -> Maybe (LList a b c) +(HeadLList i _) `pointNodeToMaybe` Nothing = Just (HeadLList i Nothing) +(HeadLList i _) `pointNodeToMaybe` (Just (NodeLList k _ _)) = Just (HeadLList i (Just k)) +(NodeLList k1 i _) `pointNodeToMaybe` Nothing = Just (NodeLList k1 i Nothing) +(NodeLList k1 i _) `pointNodeToMaybe` (Just (NodeLList k2 _ _)) = + if k1 < k2 + then Just (NodeLList k1 i (Just k2)) + else Nothing +_ `pointNodeToMaybe` _ = Nothing diff --git a/mlabs/src/Mlabs/Data/List.hs b/mlabs/src/Mlabs/Data/List.hs new file mode 100644 index 000000000..0134ee6d2 --- /dev/null +++ b/mlabs/src/Mlabs/Data/List.hs @@ -0,0 +1,82 @@ +-- | Missing plutus functions for Lists +module Mlabs.Data.List ( + take, + sortOn, + sortBy, + mapM_, + firstJustRight, + maybeRight, +) where + +import PlutusTx.Prelude hiding (mapM_, take) +import Prelude qualified as Hask (Monad, seq) + +import Mlabs.Data.Ord (comparing) + +{-# INLINEABLE take #-} + +{- | 'take' @n@, applied to a list @xs@, returns the prefix of @xs@ + of length @n@, or @xs@ itself if @n > 'length' xs@. + + >>> take 5 "Hello World!" + "Hello" + >>> take 3 [1,2,3,4,5] + [1,2,3] + >>> take 3 [1,2] + [1,2] + >>> take 3 [] + [] + >>> take (-1) [1,2] + [] + >>> take 0 [1,2] + [] + + It is an instance of the more general 'Data.List.genericTake', + in which @n@ may be of any integral type. +-} +take :: Integer -> [a] -> [a] +take n + | n <= 0 = const [] + | otherwise = \case + [] -> [] + a : as -> a : take (n - 1) as + +{-# INLINEABLE sortOn #-} + +{- | Sort a list by comparing the results of a key function applied to each + element. @sortOn f@ is equivalent to @sortBy (comparing f)@, but has the + performance advantage of only evaluating @f@ once for each element in the + input list. This is called the decorate-sort-undecorate paradigm, or + Schwartzian transform. + + Elements are arranged from lowest to highest, keeping duplicates in + the order they appeared in the input. + + >>> sortOn fst [(2, "world"), (4, "!"), (1, "Hello")] + [(1,"Hello"),(2,"world"),(4,"!")] +-} +sortOn :: Ord b => (a -> b) -> [a] -> [a] +sortOn f = + map snd . sortBy (comparing fst) . map (\x -> let y = f x in y `Hask.seq` (y, x)) + +{-# INLINEABLE mapM_ #-} +mapM_ :: Hask.Monad f => (a -> f ()) -> [a] -> f () +mapM_ f = \case + [] -> return () + a : as -> do + _ <- f a + mapM_ f as + +{-# INLINEABLE firstJustRight #-} +firstJustRight :: (a -> Maybe (Either b c)) -> [a] -> Maybe c +firstJustRight f = \case + (x : xs) -> + case f x of + Just (Right a) -> Just a + _ -> firstJustRight f xs + [] -> + Nothing + +{-# INLINEABLE maybeRight #-} +maybeRight :: Either a b -> Maybe b +maybeRight = either (const Nothing) Just diff --git a/mlabs/src/Mlabs/Data/Ord.hs b/mlabs/src/Mlabs/Data/Ord.hs new file mode 100644 index 000000000..047115d93 --- /dev/null +++ b/mlabs/src/Mlabs/Data/Ord.hs @@ -0,0 +1,19 @@ +-- | Missing plutus functions for Ord. +module Mlabs.Data.Ord ( + comparing, +) where + +import PlutusTx.Prelude (Ord (compare), Ordering) + +{-# INLINEABLE comparing #-} + +{- | + > comparing p x y = compare (p x) (p y) + + Useful combinator for use in conjunction with the @xxxBy@ family + of functions from "Data.List", for example: + + > ... sortBy (comparing fst) ... +-} +comparing :: (Ord a) => (b -> a) -> b -> b -> Ordering +comparing p x y = compare (p x) (p y) diff --git a/mlabs/src/Mlabs/Demo/Contract/Burn.hs b/mlabs/src/Mlabs/Demo/Contract/Burn.hs new file mode 100644 index 000000000..90b2a6810 --- /dev/null +++ b/mlabs/src/Mlabs/Demo/Contract/Burn.hs @@ -0,0 +1,55 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} + +module Mlabs.Demo.Contract.Burn ( + burnScrAddress, + burnValHash, +) where + +import Ledger (Address, ScriptContext, Validator, ValidatorHash, validatorHash) +import Ledger.Typed.Scripts.Validators qualified as Validators +import PlutusTx qualified +import PlutusTx.Prelude (Bool (False)) + +{-# INLINEABLE mkValidator #-} + +-- | A validator script that can be used to burn any tokens sent to it. +mkValidator :: () -> () -> ScriptContext -> Bool +mkValidator _ _ _ = False + +data Burning +instance Validators.ValidatorTypes Burning where + type DatumType Burning = () + type RedeemerType Burning = () + +burnInst :: Validators.TypedValidator Burning +burnInst = + Validators.mkTypedValidator @Burning + $$(PlutusTx.compile [||mkValidator||]) + $$(PlutusTx.compile [||wrap||]) + where + wrap = Validators.wrapValidator @() @() + +burnValidator :: Validator +burnValidator = Validators.validatorScript burnInst + +burnValHash :: Ledger.ValidatorHash +burnValHash = validatorHash burnValidator + +burnScrAddress :: Ledger.Address +burnScrAddress = Validators.validatorAddress burnInst diff --git a/mlabs/src/Mlabs/Demo/Contract/Mint.hs b/mlabs/src/Mlabs/Demo/Contract/Mint.hs new file mode 100644 index 000000000..f6ea0dc3f --- /dev/null +++ b/mlabs/src/Mlabs/Demo/Contract/Mint.hs @@ -0,0 +1,137 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} + +module Mlabs.Demo.Contract.Mint ( + curPolicy, + curSymbol, + mintContract, + mintEndpoints, + MintParams (..), + MintSchema, +) where + +import PlutusTx.Prelude hiding (Semigroup (..)) +import Prelude (Semigroup (..)) + +import Control.Monad (forever, void) +import Data.Aeson (FromJSON, ToJSON) +import Data.Text (Text) +import Data.Void (Void) +import GHC.Generics (Generic) +import Ledger qualified +import Ledger.Ada qualified as Ada +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (ScriptContext, TxInfo, TxOut, scriptContextTxInfo, txInfoMint, txInfoOutputs, txOutAddress, txOutValue) +import Ledger.Scripts (Datum (Datum), MintingPolicy, mkMintingPolicyScript) +import Ledger.Typed.Scripts qualified as Scripts +import Ledger.Value (CurrencySymbol, TokenName) +import Ledger.Value qualified as Value +import Mlabs.Demo.Contract.Burn (burnScrAddress, burnValHash) +import Plutus.Contract as Contract +import PlutusTx qualified +import Schema (ToSchema) + +------------------------------------------------------------------------------ +-- On-chain code. + +{-# INLINEABLE mkPolicy #-} + +{- | A monetary policy that mints arbitrary tokens for an equal amount of Ada. + For simplicity, the Ada are sent to a burn address. +-} +mkPolicy :: Ledger.Address -> () -> ScriptContext -> Bool +mkPolicy burnAddr _ ctx = + traceIfFalse "Insufficient Ada paid" isPaid + && traceIfFalse "Forged amount is invalid" isForgeValid + where + txInfo :: TxInfo + txInfo = scriptContextTxInfo ctx + + outputs :: [TxOut] + outputs = txInfoOutputs txInfo + + forged :: [(CurrencySymbol, TokenName, Integer)] + forged = Value.flattenValue $ txInfoMint txInfo + + forgedQty :: Integer + forgedQty = foldr (\(_, _, amt) acc -> acc + amt) 0 forged + + isToBurnAddr :: TxOut -> Bool + isToBurnAddr o = txOutAddress o == burnAddr + + isPaid :: Bool + isPaid = + let adaVal = + Ada.fromValue $ mconcat $ txOutValue <$> filter isToBurnAddr outputs + in Ada.getLovelace adaVal >= forgedQty * tokenToLovelaceXR + + isForgeValid :: Bool + isForgeValid = all isValid forged + where + isValid (_, _, amt) = amt > 0 + +curPolicy :: MintingPolicy +curPolicy = + mkMintingPolicyScript $ + $$(PlutusTx.compile [||Scripts.wrapMintingPolicy . mkPolicy||]) + `PlutusTx.applyCode` PlutusTx.liftCode burnScrAddress + +curSymbol :: CurrencySymbol +curSymbol = Ledger.scriptCurrencySymbol curPolicy + +-- For demo purposes, all tokens will be minted for a price of 1 Ada. +tokenToLovelaceXR :: Integer +tokenToLovelaceXR = 1_000_000 + +------------------------------------------------------------------------------ +-- Off-chain code. + +data MintParams = MintParams + { mpTokenName :: !TokenName + , mpAmount :: !Integer + } + deriving stock (Generic) + deriving anyclass (ToJSON, FromJSON, ToSchema) + +type MintSchema = + Endpoint "mint" MintParams + +-- | Generates tokens with the specified name/amount and burns an equal amount of Ada. +mintContract :: MintParams -> Contract w MintSchema Text () +mintContract (MintParams tn amt) = do + let payVal = Ada.lovelaceValueOf $ amt * tokenToLovelaceXR + forgeVal = Value.singleton curSymbol tn amt + lookups = Constraints.mintingPolicy curPolicy + tx = + Constraints.mustPayToOtherScript + burnValHash + (Datum $ PlutusTx.toBuiltinData ()) + payVal + <> Constraints.mustMintValue forgeVal + ledgerTx <- submitTxConstraintsWith @Void lookups tx + void $ awaitTxConfirmed $ Ledger.getCardanoTxId ledgerTx + +mintEndpoints :: Contract () MintSchema Text () +-- mintEndpoints = mint >> mintEndpoints where mint = endpoint @"mint" >>= mintContract +mintEndpoints = forever mint + where + mint = toContract $ endpoint @"mint" mintContract diff --git a/mlabs/src/Mlabs/Deploy/Governance.hs b/mlabs/src/Mlabs/Deploy/Governance.hs new file mode 100644 index 000000000..2557b8539 --- /dev/null +++ b/mlabs/src/Mlabs/Deploy/Governance.hs @@ -0,0 +1,33 @@ +module Mlabs.Deploy.Governance ( + serializeGovernance, +) where + +import PlutusTx.Prelude hiding (error) +import Prelude (FilePath, IO) + +import Mlabs.Governance.Contract.Validation + +-- import Ledger (scriptCurrencySymbol) +import Ledger.Typed.Scripts.Validators (validatorScript) + +import Mlabs.Deploy.Utils + +outDir :: FilePath +outDir = "/home/mike/dev/mlabs/contract_deploy/node_mnt/plutus_files" + +-- serializeGovernance txId txIx ownerPkh content outDir = do +serializeGovernance :: IO () +serializeGovernance = do + let acGov = + AssetClassGov + "fda1b6b487bee2e7f64ecf24d24b1224342484c0195ee1b7b943db50" -- MintingPolicy.plutus + "GOV" + validator = validatorScript $ govInstance acGov + policy = xGovMintingPolicy acGov + + -- alicePkh = "4cebc6f2a3d0111ddeb09ac48e2053b83b33b15f29182f9b528c6491" + -- xGovCurrSymbol = scriptCurrencySymbol policy + -- fstDatum = GovernanceDatum alicePkh xGovCurrSymbol + + validatorToPlutus (outDir ++ "/GovScript.plutus") validator + policyToPlutus (outDir ++ "/GovPolicy.plutus") policy diff --git a/mlabs/src/Mlabs/Deploy/Nft.hs b/mlabs/src/Mlabs/Deploy/Nft.hs new file mode 100644 index 000000000..7a71b8193 --- /dev/null +++ b/mlabs/src/Mlabs/Deploy/Nft.hs @@ -0,0 +1,43 @@ +module Mlabs.Deploy.Nft (serializeNft) where + +import PlutusTx.Prelude hiding (error) +import Prelude (IO, String) + +import Mlabs.Emulator.Types (UserId (..)) +import Mlabs.NftStateMachine.Contract.Forge as F +import Mlabs.NftStateMachine.Contract.StateMachine as SM +import Mlabs.NftStateMachine.Logic.Types + +-- import Data.ByteString.Lazy qualified as LB +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash)) +import Ledger.Typed.Scripts.Validators as VS +import Plutus.V1.Ledger.Api qualified as Plutus +import PlutusTx.Ratio qualified as R + +import Mlabs.Deploy.Utils + +serializeNft :: + BuiltinByteString -> + Integer -> + BuiltinByteString -> + BuiltinByteString -> + String -> + IO () +serializeNft txId txIx ownerPkh content outDir = do + let txOutRef = + Plutus.TxOutRef + (Plutus.TxId txId) + txIx + userId = UserId $ PaymentPubKeyHash $ Plutus.PubKeyHash ownerPkh + initNftDatum = initNft txOutRef userId content (R.reduce 1 2) (Just 1000) + nftId = nft'id initNftDatum + typedValidator = SM.scriptInstance nftId + policy = F.currencyPolicy (validatorAddress typedValidator) nftId + + -- print $ nftId'token nftId + -- BS.writeFile (outDir ++ "/t_name") (fromBuiltin content) + -- validatorToPlutus (outDir ++ "/NftScript.plutus") + -- (VS.validatorScript typedValidator) + policyToPlutus (outDir ++ "/NftPolicy.plutus") policy + +-- writeData (outDir ++ "/init-datum.json") initNftDatum diff --git a/mlabs/src/Mlabs/Deploy/Utils.hs b/mlabs/src/Mlabs/Deploy/Utils.hs new file mode 100644 index 000000000..9908e15a1 --- /dev/null +++ b/mlabs/src/Mlabs/Deploy/Utils.hs @@ -0,0 +1,82 @@ +module Mlabs.Deploy.Utils ( + validatorToPlutus, + policyToPlutus, + writeData, + toSchemeJson, +) where + +import PlutusTx.Prelude hiding (error) +import Prelude (FilePath, IO, String, error, print) + +import Data.Aeson as Json (encode) +import Data.ByteString.Lazy qualified as LB +import Data.ByteString.Short qualified as SBS + +import Cardano.Api.Shelley ( + Error (displayError), + PlutusScript (..), + PlutusScriptV1, + ScriptData (ScriptDataNumber), + ScriptDataJsonSchema (ScriptDataJsonDetailedSchema), + fromPlutusData, + scriptDataToJson, + toAlonzoData, + writeFileTextEnvelope, + ) + +import Cardano.Ledger.Alonzo.Data qualified as Alonzo +import Codec.Serialise (serialise) +import Plutus.V1.Ledger.Api (Validator) +import Plutus.V1.Ledger.Api qualified as Plutus +import PlutusTx (ToData, toData) + +validatorToPlutus :: FilePath -> Validator -> IO () +validatorToPlutus file validator = do + -- taken from here + -- https://github.com/input-output-hk/Alonzo-testnet/blob/main/resources/plutus-sources/plutus-example/app/plutus-minting-purple-example.hs + let (validatorPurpleScript, validatorAsSBS) = serializeValidator validator + case Plutus.defaultCostModelParams of + Just m -> + let getAlonzoData d = case toAlonzoData d of + Alonzo.Data pData -> pData + (logout, e) = + Plutus.evaluateScriptCounting + Plutus.Verbose + m + validatorAsSBS + [getAlonzoData (ScriptDataNumber 42)] + in do + print ("Log output" :: String) >> print logout + case e of + Left evalErr -> print ("Eval Error" :: String) >> print evalErr + Right exbudget -> print ("Ex Budget" :: String) >> print exbudget + Nothing -> error "defaultCostModelParams failed" + result <- writeFileTextEnvelope file Nothing validatorPurpleScript + case result of + Left err -> print $ displayError err + Right () -> return () + +policyToPlutus :: FilePath -> Plutus.MintingPolicy -> IO () +policyToPlutus file policy = + validatorToPlutus + file + $ Plutus.Validator $ Plutus.unMintingPolicyScript policy + +serializeValidator :: Validator -> (PlutusScript PlutusScriptV1, SBS.ShortByteString) +serializeValidator validator = + let sbs :: SBS.ShortByteString + sbs = SBS.toShort . LB.toStrict . serialise $ validator + + purpleScript :: PlutusScript PlutusScriptV1 + purpleScript = PlutusScriptSerialised sbs + in (purpleScript, sbs) + +writeData :: ToData a => FilePath -> a -> IO () +writeData file isData = LB.writeFile file (toSchemeJson isData) + +toSchemeJson :: ToData a => a -> LB.ByteString +toSchemeJson = + Json.encode + . scriptDataToJson ScriptDataJsonDetailedSchema + . fromPlutusData + . PlutusTx.toData diff --git a/mlabs/src/Mlabs/EfficientNFT/Api.hs b/mlabs/src/Mlabs/EfficientNFT/Api.hs new file mode 100644 index 000000000..3464ca517 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Api.hs @@ -0,0 +1,63 @@ +module Mlabs.EfficientNFT.Api ( + ApiUserContract, + endpoints, + NFTAppSchema, +) where + +import Control.Monad (void) +import Data.Monoid (Last (..)) +import Data.Text (Text) +import Ledger (PubKeyHash) +import Plutus.Contract (Contract, Endpoint, Promise, endpoint, type (.\/)) +import Plutus.V1.Ledger.Value (AssetClass) +import PlutusTx.Prelude + +import Mlabs.EfficientNFT.Contract.Burn (burn) +import Mlabs.EfficientNFT.Contract.ChangeOwner (changeOwner) +import Mlabs.EfficientNFT.Contract.FeeWithdraw (feeWithdraw) +import Mlabs.EfficientNFT.Contract.MarketplaceBuy (marketplaceBuy) +import Mlabs.EfficientNFT.Contract.MarketplaceDeposit (marketplaceDeposit) +import Mlabs.EfficientNFT.Contract.MarketplaceRedeem (marketplaceRedeem) +import Mlabs.EfficientNFT.Contract.MarketplaceSetPrice (marketplaceSetPrice) +import Mlabs.EfficientNFT.Contract.Mint (mint, mintWithCollection) +import Mlabs.EfficientNFT.Contract.SetPrice (setPrice) +import Mlabs.EfficientNFT.Types (ChangeOwnerParams, MintParams, NftData, SetPriceParams) +import Mlabs.Plutus.Contract (selectForever) + +-- | A common App schema works for now. +type NFTAppSchema = + -- Author Endpoints + Endpoint "mint" MintParams + .\/ Endpoint "mint-with-collection" (AssetClass, MintParams) + -- User Action Endpoints + .\/ Endpoint "change-owner" ChangeOwnerParams + .\/ Endpoint "set-price" SetPriceParams + .\/ Endpoint "marketplace-deposit" NftData + .\/ Endpoint "marketplace-redeem" NftData + .\/ Endpoint "marketplace-buy" NftData + .\/ Endpoint "marketplace-set-price" SetPriceParams + .\/ Endpoint "burn" NftData + .\/ Endpoint "fee-withdraw" [PubKeyHash] + +-- ENDPOINTS -- + +type ApiUserContract a = Contract (Last NftData) NFTAppSchema Text a + +-- | User Endpoints . +endpoints :: ApiUserContract () +endpoints = selectForever tokenEndpointsList + +-- | List of User Promises. +tokenEndpointsList :: [Promise (Last NftData) NFTAppSchema Text ()] +tokenEndpointsList = + [ void $ endpoint @"mint" mint + , void $ endpoint @"mint-with-collection" mintWithCollection + , endpoint @"change-owner" changeOwner + , void $ endpoint @"set-price" setPrice + , void $ endpoint @"marketplace-deposit" marketplaceDeposit + , void $ endpoint @"marketplace-redeem" marketplaceRedeem + , void $ endpoint @"marketplace-buy" marketplaceBuy + , void $ endpoint @"marketplace-set-price" marketplaceSetPrice + , endpoint @"burn" burn + , endpoint @"fee-withdraw" feeWithdraw + ] diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/Aux.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/Aux.hs new file mode 100644 index 000000000..9366853e2 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/Aux.hs @@ -0,0 +1,30 @@ +module Mlabs.EfficientNFT.Contract.Aux ( + getUserAddr, + getUserUtxos, + getAddrUtxos, + getFirstUtxo, +) where + +import PlutusTx.Prelude + +import Data.Map qualified as Map +import Ledger (Address, ChainIndexTxOut, TxOutRef, pubKeyHashAddress) +import Plutus.Contract qualified as Contract + +import Mlabs.EfficientNFT.Types + +-- | Get the current Wallet's publick key. +getUserAddr :: GenericContract Address +getUserAddr = (`pubKeyHashAddress` Nothing) <$> Contract.ownPaymentPubKeyHash + +-- | Get the current wallet's utxos. +getUserUtxos :: GenericContract (Map.Map TxOutRef ChainIndexTxOut) +getUserUtxos = getAddrUtxos =<< getUserAddr + +-- | Get the ChainIndexTxOut at an address. +getAddrUtxos :: Address -> GenericContract (Map.Map TxOutRef ChainIndexTxOut) +getAddrUtxos adr = Map.map fst <$> Contract.utxosTxOutTxAt adr + +-- | Get first utxo of current wallet +getFirstUtxo :: GenericContract (TxOutRef, ChainIndexTxOut) +getFirstUtxo = head . Map.toList <$> getUserUtxos diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/Burn.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/Burn.hs new file mode 100644 index 000000000..bd9442a39 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/Burn.hs @@ -0,0 +1,79 @@ +module Mlabs.EfficientNFT.Contract.Burn (burn) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Default (def) +import Data.Map qualified as Map +import Ledger ( + Extended (Finite, PosInf), + Interval (Interval), + LowerBound (LowerBound), + Redeemer (Redeemer), + UpperBound (UpperBound), + minAdaTxOut, + scriptHashAddress, + _ciTxOutValue, + ) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (scriptCurrencySymbol) +import Ledger.TimeSlot (slotToBeginPOSIXTime) +import Ledger.Typed.Scripts (Any, validatorScript) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Ada (toValue) +import Plutus.V1.Ledger.Api (toBuiltinData) +import Plutus.V1.Ledger.Value (singleton, valueOf) +import Text.Printf (printf) + +import Mlabs.EfficientNFT.Contract.Aux (getAddrUtxos, getUserUtxos) +import Mlabs.EfficientNFT.Lock (lockValidator) +import Mlabs.EfficientNFT.Token (mkTokenName, policy) +import Mlabs.EfficientNFT.Types + +burn :: NftData -> UserContract () +burn nftData = do + pkh <- Contract.ownPaymentPubKeyHash + currSlot <- Contract.currentSlot + let collection = nftData'nftCollection nftData + policy' = policy collection + curr = scriptCurrencySymbol policy' + lockValidator' = + lockValidator + (nftCollection'collectionNftCs collection) + (nftCollection'lockLockup collection) + (nftCollection'lockLockupEnd collection) + nft = nftData'nftId nftData + name = mkTokenName nft + nftValue = singleton curr name (-1) + cnftValue = singleton cnftCs cnftTn 1 + mintRedeemer = Redeemer . toBuiltinData $ BurnToken nft + cnftCs = nftCollection'collectionNftCs collection + cnftTn = nftId'collectionNftTn nft + containsCnft (_, tx) = valueOf (_ciTxOutValue tx) cnftCs cnftTn == 1 + now = slotToBeginPOSIXTime def currSlot + validRange = Interval (LowerBound (Finite now) True) (UpperBound PosInf False) + utxo' <- find containsCnft . Map.toList <$> getAddrUtxos (scriptHashAddress $ nftCollection'lockingScript collection) + (utxo, utxoIndex) <- case utxo' of + Nothing -> do + Contract.throwError "NFT not found in locking address" + Just x -> Hask.pure x + userUtxos <- getUserUtxos + let lookup = + Hask.mconcat + [ Constraints.mintingPolicy policy' + , Constraints.typedValidatorLookups lockValidator' + , Constraints.otherScript (validatorScript lockValidator') + , Constraints.unspentOutputs $ Map.insert utxo utxoIndex userUtxos + , Constraints.ownPaymentPubKeyHash pkh + ] + tx = + Hask.mconcat + [ Constraints.mustMintValueWithRedeemer mintRedeemer nftValue + , Constraints.mustBeSignedBy pkh + , Constraints.mustSpendScriptOutput utxo (Redeemer $ toBuiltinData $ Unstake (nftId'owner nft) (nftId'price nft)) + , Constraints.mustPayToPubKey pkh (cnftValue <> toValue minAdaTxOut) + , Constraints.mustValidateIn validRange + ] + void $ Contract.submitTxConstraintsWith @Any lookup tx + Contract.logInfo @Hask.String $ printf "Burn successful" diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/ChangeOwner.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/ChangeOwner.hs new file mode 100644 index 000000000..c68738763 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/ChangeOwner.hs @@ -0,0 +1,56 @@ +module Mlabs.EfficientNFT.Contract.ChangeOwner (changeOwner) where + +import PlutusTx qualified +import PlutusTx.Prelude hiding (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Void (Void) +import Ledger (Datum (Datum), Redeemer (Redeemer), scriptCurrencySymbol) +import Ledger.Constraints qualified as Constraints +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Ada (lovelaceValueOf) +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import Plutus.V1.Ledger.Value (assetClass, singleton) +import PlutusTx.Numeric.Extra (addExtend) +import Text.Printf (printf) + +import Mlabs.EfficientNFT.Contract.Aux +import Mlabs.EfficientNFT.Token +import Mlabs.EfficientNFT.Types + +changeOwner :: ChangeOwnerParams -> UserContract () +changeOwner cp = do + utxos <- getUserUtxos + let collection = nftData'nftCollection . cp'nftData $ cp + policy' = policy collection + curr = scriptCurrencySymbol policy' + oldNft = nftData'nftId . cp'nftData $ cp + newNft = oldNft {nftId'owner = cp'owner cp} + oldName = mkTokenName oldNft + newName = mkTokenName newNft + oldNftValue = singleton curr oldName (-1) + newNftValue = singleton curr newName 1 + nftPrice = nftId'price oldNft + mintRedeemer = Redeemer . toBuiltinData $ ChangeOwner oldNft (cp'owner cp) + getShare share = lovelaceValueOf $ (addExtend nftPrice * 10000) `divide` share + authorShare = getShare (addExtend . nftCollection'authorShare $ collection) + daoShare = getShare (addExtend . nftCollection'daoShare $ collection) + ownerShare = lovelaceValueOf (addExtend nftPrice) - authorShare - daoShare + datum = Datum . PlutusTx.toBuiltinData $ (curr, oldName) + lookup = + Hask.mconcat + [ Constraints.mintingPolicy policy' + , Constraints.unspentOutputs utxos + ] + tx = + Hask.mconcat + [ Constraints.mustMintValueWithRedeemer mintRedeemer (newNftValue <> oldNftValue) + , Constraints.mustPayToPubKey (cp'owner cp) newNftValue + , Constraints.mustPayWithDatumToPubKey (nftCollection'author collection) datum authorShare + , Constraints.mustPayWithDatumToPubKey (nftId'owner oldNft) datum ownerShare + , Constraints.mustPayToOtherScript (nftCollection'daoScript collection) datum daoShare + ] + void $ Contract.submitTxConstraintsWith @Void lookup tx + Contract.tell . Hask.pure $ NftData collection newNft + Contract.logInfo @Hask.String $ printf "Change owner successful: %s" (Hask.show $ assetClass curr newName) diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/FeeWithdraw.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/FeeWithdraw.hs new file mode 100644 index 000000000..3fdf1c48c --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/FeeWithdraw.hs @@ -0,0 +1,42 @@ +module Mlabs.EfficientNFT.Contract.FeeWithdraw (feeWithdraw) where + +import PlutusTx.Prelude +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Map qualified as Map +import Ledger (ChainIndexTxOut (_ciTxOutValue), PubKeyHash, Redeemer (Redeemer), scriptHashAddress) +import Ledger.Constraints qualified as Constraints +import Ledger.Typed.Scripts (Any, validatorHash, validatorScript) +import Plutus.Contract qualified as Contract +import Text.Printf (printf) + +import Mlabs.EfficientNFT.Contract.Aux (getAddrUtxos) +import Mlabs.EfficientNFT.Dao (daoValidator) +import Mlabs.EfficientNFT.Types (UserContract) +import Plutus.V1.Ledger.Api (toBuiltinData) + +feeWithdraw :: [PubKeyHash] -> UserContract () +feeWithdraw pkhs = do + let daoValidator' = daoValidator pkhs + valHash = validatorHash daoValidator' + scriptAddr = scriptHashAddress valHash + pkh <- Contract.ownPaymentPubKeyHash + utxos <- getAddrUtxos scriptAddr + let feeValues = mconcat $ map _ciTxOutValue $ Map.elems utxos + lookup = + Hask.mconcat + [ Constraints.typedValidatorLookups daoValidator' + , Constraints.otherScript (validatorScript daoValidator') + , Constraints.unspentOutputs utxos + , Constraints.ownPaymentPubKeyHash pkh + ] + tx = + Hask.mconcat + ( [ Constraints.mustBeSignedBy pkh + , Constraints.mustPayToPubKey pkh feeValues + ] + <> fmap (\utxo -> Constraints.mustSpendScriptOutput utxo (Redeemer $ toBuiltinData ())) (Map.keys utxos) + ) + void $ Contract.submitTxConstraintsWith @Any lookup tx + Contract.logInfo @Hask.String $ printf "Fee withdraw successful: %s" (Hask.show feeValues) diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceBuy.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceBuy.hs new file mode 100644 index 000000000..43148d769 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceBuy.hs @@ -0,0 +1,84 @@ +module Mlabs.EfficientNFT.Contract.MarketplaceBuy (marketplaceBuy) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Map qualified as Map +import Ledger (Datum (Datum), minAdaTxOut, scriptAddress, _ciTxOutValue) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (scriptCurrencySymbol) +import Ledger.Typed.Scripts (Any, validatorHash, validatorScript) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Ada (getLovelace, lovelaceValueOf, toValue) +import Plutus.V1.Ledger.Api (Redeemer (Redeemer), toBuiltinData) +import Plutus.V1.Ledger.Value (assetClass, singleton, valueOf) +import PlutusTx.Numeric.Extra (addExtend) +import Text.Printf (printf) + +import Mlabs.EfficientNFT.Contract.Aux +import Mlabs.EfficientNFT.Marketplace +import Mlabs.EfficientNFT.Token +import Mlabs.EfficientNFT.Types + +marketplaceBuy :: NftData -> UserContract NftData +marketplaceBuy nftData = do + pkh <- Contract.ownPaymentPubKeyHash + let policy' = policy . nftData'nftCollection $ nftData + nft = nftData'nftId nftData + curr = scriptCurrencySymbol policy' + scriptAddr = scriptAddress . validatorScript $ marketplaceValidator + containsNft (_, tx) = valueOf (_ciTxOutValue tx) curr oldName == 1 + valHash = validatorHash marketplaceValidator + nftPrice = nftId'price nft + newNft = nft {nftId'owner = pkh} + oldName = mkTokenName nft + newName = mkTokenName newNft + oldNftValue = singleton curr oldName (-1) + newNftValue = singleton curr newName 1 + mintRedeemer = Redeemer . toBuiltinData $ ChangeOwner nft pkh + getShare share = (addExtend nftPrice * share) `divide` 10000 + authorShare = getShare (addExtend . nftCollection'authorShare . nftData'nftCollection $ nftData) + daoShare = getShare (addExtend . nftCollection'daoShare . nftData'nftCollection $ nftData) + shareToSubtract v + | v < getLovelace minAdaTxOut = 0 + | otherwise = v + ownerShare = lovelaceValueOf (addExtend nftPrice - shareToSubtract authorShare - shareToSubtract daoShare) + datum = Datum . toBuiltinData $ (curr, oldName) + filterLowValue v t + | v < getLovelace minAdaTxOut = mempty + | otherwise = t (lovelaceValueOf v) + newNftData = NftData (nftData'nftCollection nftData) newNft + userUtxos <- getUserUtxos + utxo' <- find containsNft . Map.toList <$> getAddrUtxos scriptAddr + (utxo, utxoIndex) <- case utxo' of + Nothing -> Contract.throwError "NFT not found on marketplace" + Just x -> Hask.pure x + let lookup = + Hask.mconcat + [ Constraints.mintingPolicy policy' + , Constraints.typedValidatorLookups marketplaceValidator + , Constraints.otherScript (validatorScript marketplaceValidator) + , Constraints.unspentOutputs $ Map.insert utxo utxoIndex userUtxos + , Constraints.ownPaymentPubKeyHash pkh + ] + tx = + filterLowValue + daoShare + (Constraints.mustPayToOtherScript (nftCollection'daoScript . nftData'nftCollection $ nftData) datum) + <> filterLowValue + authorShare + (Constraints.mustPayWithDatumToPubKey (nftCollection'author . nftData'nftCollection $ nftData) datum) + <> Hask.mconcat + [ Constraints.mustMintValueWithRedeemer mintRedeemer (newNftValue <> oldNftValue) + , Constraints.mustSpendScriptOutput utxo (Redeemer $ toBuiltinData ()) + , Constraints.mustPayWithDatumToPubKey (nftId'owner nft) datum ownerShare + , Constraints.mustPayToOtherScript + valHash + (Datum . toBuiltinData . MarketplaceDatum $ assetClass curr newName) + (newNftValue <> toValue minAdaTxOut) + ] + void $ Contract.submitTxConstraintsWith @Any lookup tx + Contract.tell . Hask.pure $ newNftData + Contract.logInfo @Hask.String $ printf "Buy successful: %s" (Hask.show $ assetClass curr newName) + Hask.pure newNftData diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceDeposit.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceDeposit.hs new file mode 100644 index 000000000..7fd432225 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceDeposit.hs @@ -0,0 +1,48 @@ +module Mlabs.EfficientNFT.Contract.MarketplaceDeposit (marketplaceDeposit) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Ledger (Datum (Datum), minAdaTxOut) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (scriptCurrencySymbol) +import Ledger.Typed.Scripts (Any, validatorHash, validatorScript) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Ada (toValue) +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import Plutus.V1.Ledger.Value (assetClass, singleton) +import Text.Printf (printf) + +import Mlabs.EfficientNFT.Contract.Aux +import Mlabs.EfficientNFT.Marketplace +import Mlabs.EfficientNFT.Token (mkTokenName, policy) +import Mlabs.EfficientNFT.Types + +-- | Deposit nft in the marketplace +marketplaceDeposit :: NftData -> UserContract NftData +marketplaceDeposit nftData = do + let policy' = policy . nftData'nftCollection $ nftData + curr = scriptCurrencySymbol policy' + tn = mkTokenName . nftData'nftId $ nftData + nftValue = singleton curr tn 1 + valHash = validatorHash marketplaceValidator + utxos <- getUserUtxos + let lookup = + Hask.mconcat + [ Constraints.mintingPolicy policy' + , Constraints.unspentOutputs utxos + , Constraints.typedValidatorLookups marketplaceValidator + , Constraints.otherScript (validatorScript marketplaceValidator) + ] + tx = + Hask.mconcat + [ Constraints.mustPayToOtherScript + valHash + (Datum . toBuiltinData . MarketplaceDatum $ assetClass curr tn) + (nftValue <> toValue minAdaTxOut) + ] + void $ Contract.submitTxConstraintsWith @Any lookup tx + Contract.tell . Hask.pure $ nftData + Contract.logInfo @Hask.String $ printf "Deposit successful: %s" (Hask.show $ assetClass curr tn) + Hask.pure nftData diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceRedeem.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceRedeem.hs new file mode 100644 index 000000000..e605738f5 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceRedeem.hs @@ -0,0 +1,64 @@ +module Mlabs.EfficientNFT.Contract.MarketplaceRedeem (marketplaceRedeem) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Map qualified as Map +import Ledger (ChainIndexTxOut (_ciTxOutValue), Redeemer (Redeemer), minAdaTxOut, scriptHashAddress) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (scriptCurrencySymbol) +import Ledger.Typed.Scripts (Any, validatorHash, validatorScript) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Ada (toValue) +import Plutus.V1.Ledger.Api (toBuiltinData) +import Plutus.V1.Ledger.Value (assetClass, singleton, valueOf) +import Text.Printf (printf) + +import Mlabs.EfficientNFT.Contract.Aux (getAddrUtxos, getUserUtxos) +import Mlabs.EfficientNFT.Marketplace +import Mlabs.EfficientNFT.Token (mkTokenName, policy) +import Mlabs.EfficientNFT.Types + +-- | Redeem nft from the marketplace. To redeem nft it must be reminted so price is increased by 1 lovelace +marketplaceRedeem :: NftData -> UserContract NftData +marketplaceRedeem nftData = do + let collection = nftData'nftCollection nftData + policy' = policy collection + curr = scriptCurrencySymbol policy' + valHash = validatorHash marketplaceValidator + scriptAddr = scriptHashAddress valHash + newPrice = toEnum (fromEnum (nftId'price oldNft) + 1) + oldNft = nftData'nftId nftData + newNft = oldNft {nftId'price = newPrice} + oldName = mkTokenName oldNft + newName = mkTokenName newNft + oldNftValue = singleton curr oldName (-1) + newNftValue = singleton curr newName 1 + mintRedeemer = Redeemer . toBuiltinData $ ChangePrice oldNft newPrice + containsNft (_, tx) = valueOf (_ciTxOutValue tx) curr oldName == 1 + utxo' <- find containsNft . Map.toList <$> getAddrUtxos scriptAddr + (utxo, utxoIndex) <- case utxo' of + Nothing -> Contract.throwError "NFT not found on marketplace" + Just x -> Hask.pure x + pkh <- Contract.ownPaymentPubKeyHash + userUtxos <- getUserUtxos + let lookup = + Hask.mconcat + [ Constraints.mintingPolicy policy' + , Constraints.typedValidatorLookups marketplaceValidator + , Constraints.otherScript (validatorScript marketplaceValidator) + , Constraints.unspentOutputs $ Map.insert utxo utxoIndex userUtxos + , Constraints.ownPaymentPubKeyHash pkh + ] + tx = + Hask.mconcat + [ Constraints.mustMintValueWithRedeemer mintRedeemer (newNftValue <> oldNftValue) + , Constraints.mustBeSignedBy pkh + , Constraints.mustSpendScriptOutput utxo (Redeemer $ toBuiltinData ()) + , Constraints.mustPayToPubKey pkh (newNftValue <> toValue minAdaTxOut) + ] + void $ Contract.submitTxConstraintsWith @Any lookup tx + Contract.tell . Hask.pure $ NftData collection newNft + Contract.logInfo @Hask.String $ printf "Redeem successful: %s" (Hask.show $ assetClass curr newName) + Hask.pure $ NftData collection newNft diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceSetPrice.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceSetPrice.hs new file mode 100644 index 000000000..9f15df9d7 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/MarketplaceSetPrice.hs @@ -0,0 +1,67 @@ +module Mlabs.EfficientNFT.Contract.MarketplaceSetPrice (marketplaceSetPrice) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Map qualified as Map +import Ledger (Datum (Datum), Redeemer (Redeemer), minAdaTxOut, scriptHashAddress, _ciTxOutValue) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (scriptCurrencySymbol) +import Ledger.Typed.Scripts (Any, validatorHash, validatorScript) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Ada (toValue) +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import Plutus.V1.Ledger.Value (assetClass, singleton, valueOf) +import Text.Printf (printf) + +import Mlabs.EfficientNFT.Contract.Aux +import Mlabs.EfficientNFT.Marketplace +import Mlabs.EfficientNFT.Token +import Mlabs.EfficientNFT.Types + +marketplaceSetPrice :: SetPriceParams -> UserContract NftData +marketplaceSetPrice sp = do + let collection = nftData'nftCollection . sp'nftData $ sp + policy' = policy collection + curr = scriptCurrencySymbol policy' + valHash = validatorHash marketplaceValidator + scriptAddr = scriptHashAddress valHash + oldNft = nftData'nftId . sp'nftData $ sp + newNft = oldNft {nftId'price = sp'price sp} + oldName = mkTokenName oldNft + newName = mkTokenName newNft + oldNftValue = singleton curr oldName (-1) + newNftValue = singleton curr newName 1 + mintRedeemer = Redeemer . toBuiltinData $ ChangePrice oldNft (sp'price sp) + containsNft (_, tx) = valueOf (_ciTxOutValue tx) curr oldName == 1 + nftData = NftData collection newNft + utxo' <- find containsNft . Map.toList <$> getAddrUtxos scriptAddr + (utxo, utxoIndex) <- case utxo' of + Nothing -> Contract.throwError "NFT not found on marketplace" + Just x -> Hask.pure x + pkh <- Contract.ownPaymentPubKeyHash + userUtxos <- getUserUtxos + Contract.logInfo @Hask.String $ printf "Script UTXOs: %s" (Hask.show . _ciTxOutValue $ utxoIndex) + let lookup = + Hask.mconcat + [ Constraints.mintingPolicy policy' + , Constraints.typedValidatorLookups marketplaceValidator + , Constraints.otherScript (validatorScript marketplaceValidator) + , Constraints.unspentOutputs $ Map.insert utxo utxoIndex userUtxos + , Constraints.ownPaymentPubKeyHash pkh + ] + tx = + Hask.mconcat + [ Constraints.mustMintValueWithRedeemer mintRedeemer (newNftValue <> oldNftValue) + , Constraints.mustBeSignedBy pkh + , Constraints.mustSpendScriptOutput utxo (Redeemer $ toBuiltinData ()) + , Constraints.mustPayToOtherScript + valHash + (Datum . toBuiltinData . MarketplaceDatum $ assetClass curr newName) + (newNftValue <> toValue minAdaTxOut) + ] + void $ Contract.submitTxConstraintsWith @Any lookup tx + Contract.tell . Hask.pure $ nftData + Contract.logInfo @Hask.String $ printf "Marketplace set price successful: %s" (Hask.show $ assetClass curr newName) + Hask.pure nftData diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/Mint.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/Mint.hs new file mode 100644 index 000000000..7cf51c73f --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/Mint.hs @@ -0,0 +1,98 @@ +module Mlabs.EfficientNFT.Contract.Mint (mint, mintWithCollection, generateNft) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Default (def) +import Data.Text (pack) +import Data.Void (Void) +import Ledger (Datum (Datum), Redeemer (Redeemer), minAdaTxOut) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts (scriptCurrencySymbol) +import Ledger.TimeSlot (slotToBeginPOSIXTime) +import Ledger.Typed.Scripts (validatorHash) +import Plutus.Contract qualified as Contract +import Plutus.Contracts.Currency (CurrencyError, mintContract) +import Plutus.Contracts.Currency qualified as MC +import Plutus.V1.Ledger.Ada (toValue) +import Plutus.V1.Ledger.Api (Extended (Finite, PosInf), Interval (Interval), LowerBound (LowerBound), ToData (toBuiltinData), TokenName (TokenName), UpperBound (UpperBound)) +import Plutus.V1.Ledger.Value (AssetClass, assetClass, assetClassValue, singleton, unAssetClass) +import Text.Printf (printf) + +import Mlabs.EfficientNFT.Contract.Aux (getUserUtxos) +import Mlabs.EfficientNFT.Dao (daoValidator) +import Mlabs.EfficientNFT.Lock (lockValidator) +import Mlabs.EfficientNFT.Token (mkTokenName, policy) +import Mlabs.EfficientNFT.Types + +mint :: MintParams -> UserContract NftData +mint mp = do + ac <- generateNft + mintWithCollection (ac, mp) + +mintWithCollection :: (AssetClass, MintParams) -> UserContract NftData +mintWithCollection (ac, mp) = do + pkh <- Contract.ownPaymentPubKeyHash + utxos <- getUserUtxos + currSlot <- Contract.currentSlot + Contract.logInfo @Hask.String $ printf "Curr slot: %s" (Hask.show currSlot) + let now = slotToBeginPOSIXTime def currSlot + author = fromMaybe pkh $ mp'fakeAuthor mp + nft = + NftId + { nftId'price = mp'price mp + , nftId'owner = author + , nftId'collectionNftTn = snd . unAssetClass $ ac + } + collection = + NftCollection + { nftCollection'collectionNftCs = fst . unAssetClass $ ac + , nftCollection'lockLockup = mp'lockLockup mp + , nftCollection'lockLockupEnd = mp'lockLockupEnd mp + , nftCollection'lockingScript = + validatorHash $ lockValidator (fst $ unAssetClass ac) (mp'lockLockup mp) (mp'lockLockupEnd mp) + , nftCollection'author = author + , nftCollection'authorShare = mp'authorShare mp + , nftCollection'daoScript = validatorHash $ daoValidator $ mp'feeVaultKeys mp + , nftCollection'daoShare = mp'daoShare mp + } + policy' = policy collection + curr = scriptCurrencySymbol policy' + tn = mkTokenName nft + nftValue = singleton curr tn 1 + mintRedeemer = Redeemer . toBuiltinData . MintToken $ nft + nftData = NftData collection nft + lookup = + Hask.mconcat + [ Constraints.mintingPolicy policy' + , Constraints.unspentOutputs utxos + ] + tx = + Hask.mconcat + [ Constraints.mustMintValueWithRedeemer mintRedeemer nftValue + , Constraints.mustPayToPubKey pkh (nftValue <> toValue minAdaTxOut) + , Constraints.mustPayToOtherScript + (nftCollection'lockingScript collection) + (Datum $ toBuiltinData $ LockDatum curr currSlot (snd $ unAssetClass ac)) + (assetClassValue ac 1 <> toValue minAdaTxOut) + , Constraints.mustValidateIn $ + Interval + (LowerBound (Finite now) True) + (UpperBound PosInf False) + ] + void $ Contract.submitTxConstraintsWith @Void lookup tx + Contract.tell . Hask.pure $ nftData + Contract.logInfo @Hask.String $ Hask.show nft + Contract.logInfo @Hask.String $ printf "Mint successful: %s" (Hask.show $ assetClass curr tn) + Hask.pure nftData + +generateNft :: UserContract AssetClass +generateNft = do + self <- Contract.ownPaymentPubKeyHash + let tn = TokenName "NFT" + x <- + Contract.mapError + (pack . Hask.show @CurrencyError) + (mintContract self [(tn, 1)]) + return $ assetClass (MC.currencySymbol x) tn diff --git a/mlabs/src/Mlabs/EfficientNFT/Contract/SetPrice.hs b/mlabs/src/Mlabs/EfficientNFT/Contract/SetPrice.hs new file mode 100644 index 000000000..1c3d0e9ee --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Contract/SetPrice.hs @@ -0,0 +1,50 @@ +module Mlabs.EfficientNFT.Contract.SetPrice (setPrice) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Void (Void) +import Ledger (Redeemer (Redeemer), minAdaTxOut, scriptCurrencySymbol) +import Ledger.Constraints qualified as Constraints +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Ada (toValue) +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import Plutus.V1.Ledger.Value (assetClass, singleton) +import Text.Printf (printf) + +import Mlabs.EfficientNFT.Token +import Mlabs.EfficientNFT.Types +import Mlabs.NFT.Contract.Aux (getUserUtxos) + +setPrice :: SetPriceParams -> UserContract NftData +setPrice sp = do + pkh <- Contract.ownPaymentPubKeyHash + utxos <- getUserUtxos + let collection = nftData'nftCollection . sp'nftData $ sp + policy' = policy collection + curr = scriptCurrencySymbol policy' + oldNft = nftData'nftId . sp'nftData $ sp + newNft = oldNft {nftId'price = sp'price sp} + oldName = mkTokenName oldNft + newName = mkTokenName newNft + oldNftValue = singleton curr oldName (-1) + newNftValue = singleton curr newName 1 + mintRedeemer = Redeemer . toBuiltinData $ ChangePrice oldNft (sp'price sp) + nftData = NftData collection newNft + lookup = + Hask.mconcat + [ Constraints.mintingPolicy policy' + , Constraints.unspentOutputs utxos + , Constraints.ownPaymentPubKeyHash pkh + ] + tx = + Hask.mconcat + [ Constraints.mustMintValueWithRedeemer mintRedeemer (newNftValue <> oldNftValue) + , Constraints.mustPayToPubKey pkh (newNftValue <> toValue minAdaTxOut) + , Constraints.mustBeSignedBy pkh + ] + void $ Contract.submitTxConstraintsWith @Void lookup tx + Contract.tell . Hask.pure $ nftData + Contract.logInfo @Hask.String $ printf "Set price successful: %s" (Hask.show $ assetClass curr newName) + Hask.pure nftData diff --git a/mlabs/src/Mlabs/EfficientNFT/Dao.hs b/mlabs/src/Mlabs/EfficientNFT/Dao.hs new file mode 100644 index 000000000..5a2d2acdb --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Dao.hs @@ -0,0 +1,25 @@ +module Mlabs.EfficientNFT.Dao (mkValidator, daoValidator) where + +import Ledger (txSignedBy) +import Ledger.Typed.Scripts (Any, TypedValidator, unsafeMkTypedValidator, wrapValidator) +import Plutus.V1.Ledger.Api (PubKeyHash, ScriptContext, mkValidatorScript, scriptContextTxInfo) +import PlutusTx qualified +import PlutusTx.Prelude + +mkValidator :: [PubKeyHash] -> BuiltinData -> BuiltinData -> ScriptContext -> Bool +mkValidator pkhs _ _ ctx = traceIfFalse "Must be sighned by key from list" checkSigned + where + checkSigned :: Bool + checkSigned = any (txSignedBy (scriptContextTxInfo ctx)) pkhs + +daoValidator :: [PubKeyHash] -> TypedValidator Any +daoValidator pkhs = unsafeMkTypedValidator v + where + v = + mkValidatorScript + ( $$(PlutusTx.compile [||wrap||]) + `PlutusTx.applyCode` ( $$(PlutusTx.compile [||mkValidator||]) + `PlutusTx.applyCode` PlutusTx.liftCode pkhs + ) + ) + wrap = wrapValidator diff --git a/mlabs/src/Mlabs/EfficientNFT/Lock.hs b/mlabs/src/Mlabs/EfficientNFT/Lock.hs new file mode 100644 index 000000000..6de30706c --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Lock.hs @@ -0,0 +1,158 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE LambdaCase #-} + +module Mlabs.EfficientNFT.Lock (mkValidator, lockValidator) where + +import PlutusTx qualified +import PlutusTx.Prelude + +import Data.Default (def) +import Ledger ( + CurrencySymbol, + Extended (Finite), + LowerBound (LowerBound), + PaymentPubKeyHash, + ScriptContext, + Slot (Slot), + TxInInfo (txInInfoResolved), + TxInfo (txInfoMint, txInfoOutputs, txInfoValidRange), + TxOut (txOutDatumHash, txOutValue), + findDatum, + findOwnInput, + getContinuingOutputs, + getDatum, + ivFrom, + mkValidatorScript, + scriptContextTxInfo, + txSignedBy, + unPaymentPubKeyHash, + ) +import Ledger.TimeSlot (posixTimeToEnclosingSlot) +import Ledger.Typed.Scripts (Any, TypedValidator, unsafeMkTypedValidator, wrapValidator) +import Ledger.Value (Value (getValue), valueOf) +import PlutusTx.AssocMap qualified as AssocMap +import PlutusTx.Natural (Natural) + +import Mlabs.EfficientNFT.Token (mkTokenName) +import Mlabs.EfficientNFT.Types + +{-# INLINEABLE mkValidator #-} +mkValidator :: CurrencySymbol -> Integer -> Slot -> LockDatum -> LockAct -> ScriptContext -> Bool +mkValidator _ lockup lockupEnd inDatum act ctx = + case act of + Unstake pkh price -> + traceIfFalse "CO must not exists" checkNoCO + && traceIfFalse "sgNFT must be burned" (checkSgBurned price pkh) + && if ld'entered inDatum < lockupEnd + then traceIfFalse "Current slot smaller than lockupEnd" checkCurrentSlotFirst + else traceIfFalse "Current slot smaller than lockup+entered" checkCurrentSlot + Restake pkh price -> + traceIfFalse "Cannot mint sg" checkNoSgMinted + && traceIfFalse "Values in CO cannot change" checkSameCOValues + && traceIfFalse "Owner must sign the transaction" (txSignedBy info . unPaymentPubKeyHash $ pkh) + && traceIfFalse "Input does not contain sg" (checkInputContainsSg price pkh) + && if ld'entered inDatum < lockupEnd + then + traceIfFalse "Current slot smaller than lockupEnd" checkCurrentSlotFirst + && traceIfFalse "Inconsistent datum" checkConsistentDatumRestakeFirst + else + traceIfFalse "Current slot smaller than lockup+entered" checkCurrentSlot + && traceIfFalse "Inconsistent datum" checkConsistentDatumRestake + where + -- Helpers + traceIfNothing :: BuiltinString -> Maybe a -> a + traceIfNothing err = fromMaybe (traceError err) + -- traceIfNothing _ = fromMaybe (error ()) + + -- Bindings + + inTx :: TxOut + inTx = txInInfoResolved . traceIfNothing "Own input missing" . findOwnInput $ ctx + + outTx :: TxOut + outTx = + ( \case + [] -> traceError "No CO" + [tx] -> tx + _ -> traceError "More than one CO" + ) + . getContinuingOutputs + $ ctx + + outDatum :: LockDatum + outDatum = + traceIfNothing "Invalid CO datum format" + . PlutusTx.fromBuiltinData + . getDatum + . traceIfNothing "Missing output datum" + . flip findDatum info + . traceIfNothing "Missing output datum hash" + . txOutDatumHash + $ outTx + + currentSlot :: Slot + currentSlot = + let extract (LowerBound (Finite x) _) = x + extract _ = traceError "Valid range beginning must be finite" + in posixTimeToEnclosingSlot def . extract . ivFrom . txInfoValidRange $ info + + info :: TxInfo + info = scriptContextTxInfo ctx + + -- Checks + + -- Checks that value in CO does not change + checkSameCOValues :: Bool + checkSameCOValues = txOutValue inTx == txOutValue outTx + + -- Checks that no Seabug NFT is minted + checkNoSgMinted :: Bool + checkNoSgMinted = isNothing . AssocMap.lookup (ld'sgNft inDatum) . getValue . txInfoMint $ info + + -- Checks that input contains corresponding Seabug NFT + checkInputContainsSg :: Natural -> PaymentPubKeyHash -> Bool + checkInputContainsSg price owner = + let tn = mkTokenName $ NftId (ld'underlyingTn inDatum) price owner + containsSg tx = valueOf (txOutValue tx) (ld'sgNft inDatum) tn == 1 + in any containsSg . txInfoOutputs $ info + + -- Checks that entered slot is properly updated + checkConsistentDatumRestake :: Bool + checkConsistentDatumRestake = + let validDatum = inDatum {ld'entered = ld'entered inDatum + Slot lockup} + in validDatum == outDatum + + -- Checks that entered slot is properly updated when restaking for the first time + checkConsistentDatumRestakeFirst :: Bool + checkConsistentDatumRestakeFirst = + let validDatum = inDatum {ld'entered = lockupEnd} + in validDatum == outDatum + + checkCurrentSlot :: Bool + checkCurrentSlot = currentSlot > Slot lockup + ld'entered inDatum + + checkCurrentSlotFirst :: Bool + checkCurrentSlotFirst = currentSlot > lockupEnd + + checkNoCO :: Bool + checkNoCO = null . getContinuingOutputs $ ctx + + checkSgBurned :: Natural -> PaymentPubKeyHash -> Bool + checkSgBurned price owner = + let tn = mkTokenName $ NftId (ld'underlyingTn inDatum) price owner + in valueOf (txInfoMint info) (ld'sgNft inDatum) tn == (-1) + +lockValidator :: CurrencySymbol -> Integer -> Slot -> TypedValidator Any +lockValidator underlyingCs lockup lockupEnd = unsafeMkTypedValidator v + where + v = + mkValidatorScript + ( $$(PlutusTx.compile [||wrap||]) + `PlutusTx.applyCode` ( $$(PlutusTx.compile [||mkValidator||]) + `PlutusTx.applyCode` PlutusTx.liftCode underlyingCs + `PlutusTx.applyCode` PlutusTx.liftCode lockup + `PlutusTx.applyCode` PlutusTx.liftCode lockupEnd + ) + ) + wrap = wrapValidator @LockDatum @LockAct diff --git a/mlabs/src/Mlabs/EfficientNFT/Marketplace.hs b/mlabs/src/Mlabs/EfficientNFT/Marketplace.hs new file mode 100644 index 000000000..7bbcb67c9 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Marketplace.hs @@ -0,0 +1,35 @@ +module Mlabs.EfficientNFT.Marketplace (mkValidator, marketplaceValidator) where + +import PlutusTx qualified +import PlutusTx.Prelude + +import Ledger ( + ScriptContext, + TxInfo (txInfoMint), + mkValidatorScript, + scriptContextTxInfo, + ) +import Ledger.Typed.Scripts (Any, TypedValidator, unsafeMkTypedValidator, wrapValidator) +import Ledger.Value (assetClassValueOf) + +import Mlabs.EfficientNFT.Types + +-- | An escrow-like validator, that holds an NFT until sold or pulled out +{-# INLINEABLE mkValidator #-} +mkValidator :: MarketplaceDatum -> BuiltinData -> ScriptContext -> Bool +mkValidator datum _ ctx = + traceIfFalse "All spent tokens must be reminted" checkRemint + where + info :: TxInfo + info = scriptContextTxInfo ctx + + checkRemint :: Bool + checkRemint = assetClassValueOf (txInfoMint info) (getMarketplaceDatum datum) == -1 + +marketplaceValidator :: TypedValidator Any +marketplaceValidator = unsafeMkTypedValidator v + where + v = + mkValidatorScript + ($$(PlutusTx.compile [||wrap||]) `PlutusTx.applyCode` $$(PlutusTx.compile [||mkValidator||])) + wrap = wrapValidator diff --git a/mlabs/src/Mlabs/EfficientNFT/Token.hs b/mlabs/src/Mlabs/EfficientNFT/Token.hs new file mode 100644 index 000000000..7a4f0db9e --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Token.hs @@ -0,0 +1,182 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE TypeApplications #-} + +module Mlabs.EfficientNFT.Token ( + mkPolicy, + policy, + mkTokenName, +) where + +import Ledger ( + CurrencySymbol, + Datum (Datum), + MintingPolicy, + PaymentPubKeyHash (unPaymentPubKeyHash), + ScriptContext, + TxInfo (txInfoMint, txInfoOutputs), + TxOut (TxOut, txOutAddress, txOutValue), + ValidatorHash, + findDatum, + minAdaTxOut, + ownCurrencySymbol, + pubKeyHashAddress, + scriptContextTxInfo, + txSignedBy, + ) +import Ledger.Ada qualified as Ada +import Ledger.Address ( + scriptHashAddress, + ) +import Ledger.Scripts qualified as Scripts +import Ledger.Typed.Scripts (wrapMintingPolicy) +import Ledger.Value (TokenName (TokenName), valueOf) +import Ledger.Value qualified as Value +import Mlabs.EfficientNFT.Types ( + MintAct (..), + NftCollection (..), + NftId, + hash, + nftId'collectionNftTn, + nftId'owner, + nftId'price, + ) +import PlutusTx qualified +import PlutusTx.AssocMap qualified as Map +import PlutusTx.Natural (Natural) +import PlutusTx.Prelude + +{-# INLINEABLE mkPolicy #-} +mkPolicy :: + CurrencySymbol -> + ValidatorHash -> + PaymentPubKeyHash -> + Natural -> + ValidatorHash -> + Natural -> + MintAct -> + ScriptContext -> + Bool +mkPolicy collectionNftCs lockingScript author authorShare daoScript daoShare mintAct ctx = + case mintAct of + MintToken nft -> + traceIfFalse "Exactly one NFT must be minted" (checkMint nft) + && traceIfFalse "Underlying NFT must be locked" (checkCollectionNftBurned nft) + ChangePrice nft newPrice -> + traceIfFalse + "Exactly one new token must be minted and exactly one old burnt" + (checkMintAndBurn nft newPrice (nftId'owner nft)) + && traceIfFalse "Owner must sign the transaction" (txSignedBy info . unPaymentPubKeyHash . nftId'owner $ nft) + ChangeOwner nft newOwner -> + traceIfFalse + "Exactly one new token must be minted and exactly one old burnt" + (checkMintAndBurn nft (nftId'price nft) newOwner) + && traceIfFalse "Royalities not paid" (checkPartiesGotCorrectPayments nft) + BurnToken nft -> + traceIfFalse "NFT must be burned" (checkBurn nft) + && traceIfFalse "Owner must sign the transaction" (txSignedBy info . unPaymentPubKeyHash . nftId'owner $ nft) + && traceIfFalse "Underlying NFT must be unlocked" (checkUnlockNft nft) + where + !info = scriptContextTxInfo ctx + info :: TxInfo + -- ! force evaluation of `ownCs` causes policy compilation error + ownCs = ownCurrencySymbol ctx + ownCs :: CurrencySymbol + + !mintedValue = txInfoMint info + + -- Check if only one token is minted and name is correct + checkMint :: NftId -> Bool + checkMint nft = + let newName = mkTokenName nft + valMap = Value.getValue mintedValue + tokens = Map.toList $ fromMaybe (traceError "unreachable") $ Map.lookup ownCs valMap + in case tokens of + [(tn, amt)] -> tn == newName && amt == 1 + _ -> False + + -- Check if the old token is burnt and new is minted with correct name + checkMintAndBurn :: NftId -> Natural -> PaymentPubKeyHash -> Bool + checkMintAndBurn nft newPrice newOwner = + let minted = Map.toList <$> (Map.lookup ownCs . Value.getValue . txInfoMint $ info) + oldName = mkTokenName nft + newName = mkTokenName nft {nftId'price = newPrice, nftId'owner = newOwner} + in case minted of + Just [(tokenName1, tnAmt1), (tokenName2, tnAmt2)] + | tokenName1 == oldName && tokenName2 == newName -> tnAmt1 == -1 && tnAmt2 == 1 + | tokenName2 == oldName && tokenName1 == newName -> tnAmt2 == -1 && tnAmt1 == 1 + _ -> False + + checkBurn :: NftId -> Bool + checkBurn nft = + let oldName = mkTokenName nft + valMap = Value.getValue mintedValue + in Map.singleton oldName (negate 1) == fromMaybe (traceError "unreachable") (Map.lookup ownCs valMap) + + -- Check if collection nft is burned + checkCollectionNftBurned :: NftId -> Bool + checkCollectionNftBurned nft = + let lockingAddress = scriptHashAddress lockingScript + containsCollectonNft tx = + txOutAddress tx == lockingAddress + && Value.valueOf (txOutValue tx) collectionNftCs (nftId'collectionNftTn nft) == 1 + in any containsCollectonNft (txInfoOutputs info) + + checkUnlockNft :: NftId -> Bool + checkUnlockNft nft = + let lockingAddress = scriptHashAddress lockingScript + containsCollectonNft tx = + txOutAddress tx /= lockingAddress + && Value.valueOf (txOutValue tx) collectionNftCs (nftId'collectionNftTn nft) == 1 + in any containsCollectonNft (txInfoOutputs info) + + -- Check that all parties received corresponding payments, + -- and the payment utxos have the correct datum attached + checkPartiesGotCorrectPayments :: NftId -> Bool + checkPartiesGotCorrectPayments nft = + let outs = txInfoOutputs info + price' = fromEnum $ nftId'price nft + royalty' = fromEnum authorShare + mpShare = fromEnum daoShare + + shareToSubtract v + | v < Ada.getLovelace minAdaTxOut = 0 + | otherwise = v + + authorAddr = pubKeyHashAddress author Nothing + authorShareVal = (price' * royalty') `divide` 100_00 + + daoAddr = scriptHashAddress daoScript + daoShareVal = (price' * mpShare) `divide` 100_00 + + ownerAddr = pubKeyHashAddress (nftId'owner nft) Nothing + ownerShare = price' - shareToSubtract authorShareVal - shareToSubtract daoShareVal + + curSymDatum = Datum $ PlutusTx.toBuiltinData (ownCs, mkTokenName nft) + + -- Don't check royalties when lower than min ada + filterLowValue v cond + | v < Ada.getLovelace minAdaTxOut = True + | otherwise = any (checkPaymentTxOut cond v) outs + + checkPaymentTxOut addr val (TxOut addr' val' dh) = + addr == addr' && val == valueOf val' Ada.adaSymbol Ada.adaToken + && (dh >>= \dh' -> findDatum dh' info) == Just curSymDatum + in filterLowValue daoShareVal daoAddr + && filterLowValue authorShareVal authorAddr + && any (checkPaymentTxOut ownerAddr ownerShare) outs + +{-# INLINEABLE mkTokenName #-} +mkTokenName :: NftId -> TokenName +mkTokenName = TokenName . hash + +policy :: NftCollection -> MintingPolicy +policy NftCollection {..} = + Scripts.mkMintingPolicyScript $ + $$(PlutusTx.compile [||\a b c d e f -> wrapMintingPolicy (mkPolicy a b c d e f)||]) + `PlutusTx.applyCode` PlutusTx.liftCode nftCollection'collectionNftCs + `PlutusTx.applyCode` PlutusTx.liftCode nftCollection'lockingScript + `PlutusTx.applyCode` PlutusTx.liftCode nftCollection'author + `PlutusTx.applyCode` PlutusTx.liftCode nftCollection'authorShare + `PlutusTx.applyCode` PlutusTx.liftCode nftCollection'daoScript + `PlutusTx.applyCode` PlutusTx.liftCode nftCollection'daoShare diff --git a/mlabs/src/Mlabs/EfficientNFT/Types.hs b/mlabs/src/Mlabs/EfficientNFT/Types.hs new file mode 100644 index 000000000..f28dd7a64 --- /dev/null +++ b/mlabs/src/Mlabs/EfficientNFT/Types.hs @@ -0,0 +1,553 @@ +{-# LANGUAGE DerivingVia #-} + +module Mlabs.EfficientNFT.Types ( + GenericContract, + UserContract, + MintParams (..), + NftId (..), + NftCollection (..), + NftData (..), + SetPriceParams (..), + ChangeOwnerParams (..), + MintAct (..), + Hashable (..), + LockAct (..), + LockDatum (..), + MarketplaceDatum (..), +) where + +import PlutusTx qualified +import PlutusTx.Prelude +import Prelude qualified as Hask + +import Data.Aeson (FromJSON, ToJSON) +import Data.Monoid (Last) +import Data.Text (Text) +import GHC.Generics (Generic) +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash), Slot, ValidatorHash (ValidatorHash)) +import Plutus.Contract (Contract) +import Plutus.V1.Ledger.Api (fromBuiltinData, toBuiltinData, unsafeFromBuiltinData) +import Plutus.V1.Ledger.Crypto (PubKeyHash (PubKeyHash)) +import Plutus.V1.Ledger.Value (AssetClass (AssetClass), CurrencySymbol (CurrencySymbol), TokenName (TokenName)) +import PlutusTx.Builtins (blake2b_256, matchData', matchList) +import PlutusTx.Builtins.Internal (equalsInteger, ifThenElse, mkCons, mkConstr, mkNilData, unitval, unsafeDataAsConstr) +import PlutusTx.Builtins.Internal qualified as Internal +import PlutusTx.ErrorCodes (reconstructCaseError) +import PlutusTx.Natural (Natural) +import Schema (ToSchema) + +-- | Parameters that need to be submitted when minting a new NFT. +data MintParams = MintParams + { -- | Shares retained by author. + mp'authorShare :: Natural + , mp'daoShare :: Natural + , -- | Listing price of the NFT, in Lovelace. + mp'price :: Natural + , mp'lockLockup :: Integer + , mp'lockLockupEnd :: Slot + , mp'fakeAuthor :: Maybe PaymentPubKeyHash + , mp'feeVaultKeys :: [PubKeyHash] + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +data NftId = NftId + { nftId'collectionNftTn :: TokenName + , nftId'price :: Natural + , nftId'owner :: PaymentPubKeyHash + } + deriving stock (Hask.Show, Generic, Hask.Eq, Hask.Ord) + deriving anyclass (FromJSON, ToJSON) + +instance PlutusTx.ToData NftId where + {-# INLINEABLE toBuiltinData #-} + toBuiltinData (NftId tn price owner) = + mkConstr 0 $ + mkCons (toBuiltinData tn) $ + mkCons (toBuiltinData price) $ + mkCons (toBuiltinData owner) $ + mkNilData unitval + +instance PlutusTx.FromData NftId where + {-# INLINEABLE fromBuiltinData #-} + fromBuiltinData dData = + let cons3 collectionTn price owner lst = + matchList + lst + ( pure NftId + <*> fromBuiltinData collectionTn + <*> fromBuiltinData price + <*> fromBuiltinData owner + ) + (\_ _ -> Nothing) + cons2 collectionTn price lst = + matchList + lst + Nothing + (cons3 collectionTn price) + cons1 collectionTn lst = + matchList + lst + Nothing + (cons2 collectionTn) + cons0 lst = + matchList + lst + Nothing + cons1 + matchCase constrIndex lst = + ifThenElse + (constrIndex `equalsInteger` 0) + (Hask.const (cons0 lst)) + (Hask.const Nothing) + unitval + in matchData' + dData + matchCase + (Hask.const Nothing) + (Hask.const Nothing) + (Hask.const Nothing) + (Hask.const Nothing) + +instance PlutusTx.UnsafeFromData NftId where + {-# INLINEABLE unsafeFromBuiltinData #-} + unsafeFromBuiltinData bData = + let constr = unsafeDataAsConstr bData + constrIndex = Internal.fst constr + lst1 = Internal.snd constr + lst2 = Internal.tail lst1 + lst3 = Internal.tail lst2 + collectionTn = Internal.head lst1 + price = Internal.head lst2 + owner = Internal.head lst3 + val = + NftId + (unsafeFromBuiltinData collectionTn) + (unsafeFromBuiltinData price) + (unsafeFromBuiltinData owner) + in ifThenElse + (constrIndex `equalsInteger` 0) + (Hask.const val) + (Hask.const (traceError reconstructCaseError)) + unitval + +data NftCollection = NftCollection + { nftCollection'collectionNftCs :: CurrencySymbol + , nftCollection'lockLockup :: Integer + , nftCollection'lockLockupEnd :: Slot + , nftCollection'lockingScript :: ValidatorHash + , nftCollection'author :: PaymentPubKeyHash + , nftCollection'authorShare :: Natural + , nftCollection'daoScript :: ValidatorHash + , nftCollection'daoShare :: Natural + } + deriving stock (Hask.Show, Generic, Hask.Eq, Hask.Ord) + deriving anyclass (FromJSON, ToJSON) + +data NftData = NftData + { nftData'nftCollection :: NftCollection + , nftData'nftId :: NftId + } + deriving stock (Hask.Show, Generic, Hask.Eq, Hask.Ord) + deriving anyclass (FromJSON, ToJSON) + +data SetPriceParams = SetPriceParams + { -- | Token which price is set. + sp'nftData :: NftData + , -- | New price, in Lovelace. + sp'price :: Natural + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +data ChangeOwnerParams = ChangeOwnerParams + { -- | Token which owner is set. + cp'nftData :: NftData + , -- | New Owner + cp'owner :: PaymentPubKeyHash + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +type GenericContract a = forall w s. Contract w s Text a +type UserContract a = forall s. Contract (Last NftData) s Text a + +data MintAct + = MintToken NftId + | ChangePrice NftId Natural + | ChangeOwner NftId PaymentPubKeyHash + | BurnToken NftId + deriving stock (Hask.Show) + +instance PlutusTx.ToData MintAct where + {-# INLINEABLE toBuiltinData #-} + toBuiltinData (MintToken nft) = + mkConstr 0 $ + mkCons (toBuiltinData nft) $ + mkNilData unitval + toBuiltinData (ChangePrice nft price) = + mkConstr 1 $ + mkCons (toBuiltinData nft) $ + mkCons (toBuiltinData price) $ + mkNilData unitval + toBuiltinData (ChangeOwner nft pkh) = + mkConstr 2 $ + mkCons (toBuiltinData nft) $ + mkCons (toBuiltinData pkh) $ + mkNilData unitval + toBuiltinData (BurnToken nft) = + mkConstr 3 $ + mkCons (toBuiltinData nft) $ + mkNilData unitval + +instance PlutusTx.FromData MintAct where + {-# INLINEABLE PlutusTx.fromBuiltinData #-} + fromBuiltinData bData = + let matchMintToken constrIndex lst = + ifThenElse + (constrIndex `equalsInteger` 0) + (Hask.const (consMintToken0 lst)) + (Hask.const (matchChangePrice constrIndex lst)) + unitval + consMintToken0 lst = + matchList + lst + Nothing + consMintToken1 + consMintToken1 nft lst = + matchList + lst + ( pure MintToken + <*> fromBuiltinData nft + ) + (\_ _ -> Nothing) + matchChangePrice constrIndex lst = + ifThenElse + (constrIndex `equalsInteger` 1) + (Hask.const (consChangePrice0 lst)) + (Hask.const (matchChangeOwner constrIndex lst)) + unitval + consChangePrice0 lst = + matchList + lst + Nothing + consChangePrice1 + consChangePrice1 nft lst = + matchList + lst + Nothing + (consChangePrice2 nft) + consChangePrice2 nft price lst = + matchList + lst + ( pure ChangePrice + <*> fromBuiltinData nft + <*> fromBuiltinData price + ) + (\_ _ -> Nothing) + matchChangeOwner constrIndex lst = + ifThenElse + (constrIndex `equalsInteger` 2) + (Hask.const (consChangeOwner0 lst)) + (Hask.const (matchBurnToken constrIndex lst)) + unitval + consChangeOwner0 lst = + matchList + lst + Nothing + consChangeOwner1 + consChangeOwner1 nft lst = + matchList + lst + Nothing + (consChangeOwner2 nft) + consChangeOwner2 nft pkh lst = + matchList + lst + ( pure ChangeOwner + <*> fromBuiltinData nft + <*> fromBuiltinData pkh + ) + (\_ _ -> Nothing) + matchBurnToken constrIndex lst = + ifThenElse + (constrIndex `equalsInteger` 3) + (Hask.const (consBurnToken0 lst)) + (Hask.const (traceError reconstructCaseError)) + unitval + consBurnToken0 lst = + matchList + lst + Nothing + consBurnToken1 + consBurnToken1 nft lst = + matchList + lst + ( pure BurnToken + <*> fromBuiltinData nft + ) + (\_ _ -> Nothing) + in matchData' + bData + matchMintToken + (Hask.const Nothing) + (Hask.const Nothing) + (Hask.const Nothing) + (Hask.const Nothing) + +instance PlutusTx.UnsafeFromData MintAct where + {-# INLINEABLE unsafeFromBuiltinData #-} + unsafeFromBuiltinData bData = + let constr = unsafeDataAsConstr bData + constrIndex = Internal.fst constr + lst1 = Internal.snd constr + nft = Internal.head lst1 + matchMintToken = MintToken (unsafeFromBuiltinData nft) + matchBurnToken = BurnToken (unsafeFromBuiltinData nft) + fallthrough1 = + ifThenElse + (constrIndex `equalsInteger` 3) + (Hask.const matchBurnToken) + (Hask.const fallthrough2) + unitval + fallthrough2 = + let lst2 = Internal.tail lst1 + priceOrPkh = Internal.head lst2 + matchChangePrice = + ChangePrice + (unsafeFromBuiltinData nft) + (unsafeFromBuiltinData priceOrPkh) + matchChangeOwner = + ChangeOwner + (unsafeFromBuiltinData nft) + (unsafeFromBuiltinData priceOrPkh) + fallthrough3 = + ifThenElse + (constrIndex `equalsInteger` 2) + (Hask.const matchChangeOwner) + (Hask.const (traceError reconstructCaseError)) + unitval + in ifThenElse + (constrIndex `equalsInteger` 1) + (Hask.const matchChangePrice) + (Hask.const fallthrough3) + unitval + in ifThenElse + (constrIndex `equalsInteger` 0) + (Hask.const matchMintToken) + (Hask.const fallthrough1) + unitval + +data LockAct + = Unstake PaymentPubKeyHash Natural + | Restake PaymentPubKeyHash Natural + deriving stock (Hask.Show) + +instance PlutusTx.ToData LockAct where + {-# INLINEABLE toBuiltinData #-} + toBuiltinData (Unstake pkh price) = + mkConstr 0 $ + mkCons (toBuiltinData pkh) $ + mkCons (toBuiltinData price) $ + mkNilData unitval + toBuiltinData (Restake pkh price) = + mkConstr 1 $ + mkCons (toBuiltinData pkh) $ + mkCons (toBuiltinData price) $ + mkNilData unitval + +instance PlutusTx.FromData LockAct where + {-# INLINEABLE fromBuiltinData #-} + fromBuiltinData bData = + let matchUnstake constrIndex lst = + ifThenElse + (constrIndex `equalsInteger` 0) + (Hask.const (cons0 Unstake lst)) + (Hask.const (matchRestake constrIndex lst)) + unitval + matchRestake constrIndex lst = + ifThenElse + (constrIndex `equalsInteger` 1) + (Hask.const (cons0 Restake lst)) + (Hask.const Nothing) + unitval + cons0 f lst = + matchList + lst + Nothing + (cons1 f) + cons1 f pkh lst = + matchList + lst + Nothing + (cons2 f pkh) + cons2 f pkh price lst = + matchList + lst + ( pure f + <*> fromBuiltinData pkh + <*> fromBuiltinData price + ) + (\_ _ -> Nothing) + in matchData' + bData + matchUnstake + (Hask.const Nothing) + (Hask.const Nothing) + (Hask.const Nothing) + (Hask.const Nothing) + +instance PlutusTx.UnsafeFromData LockAct where + {-# INLINEABLE unsafeFromBuiltinData #-} + unsafeFromBuiltinData bData = + let constr = unsafeDataAsConstr bData + constrIndex = Internal.fst constr + lst1 = Internal.snd constr + lst2 = Internal.tail lst1 + pkh = Internal.head lst1 + price = Internal.head lst2 + restake = + Restake + (unsafeFromBuiltinData pkh) + (unsafeFromBuiltinData price) + unstake = + Unstake + (unsafeFromBuiltinData pkh) + (unsafeFromBuiltinData price) + fallthrough = + ifThenElse + (constrIndex `equalsInteger` 1) + (Hask.const restake) + (Hask.const (traceError reconstructCaseError)) + unitval + in ifThenElse + (constrIndex `equalsInteger` 0) + (Hask.const unstake) + (Hask.const fallthrough) + unitval + +data LockDatum = LockDatum + { ld'sgNft :: CurrencySymbol + , ld'entered :: Slot + , ld'underlyingTn :: TokenName + } + deriving stock (Hask.Show) + +instance PlutusTx.ToData LockDatum where + {-# INLINEABLE toBuiltinData #-} + toBuiltinData (LockDatum sgNft entered underlyingTn) = + mkConstr 0 $ + mkCons (toBuiltinData sgNft) $ + mkCons (toBuiltinData entered) $ + mkCons (toBuiltinData underlyingTn) $ + mkNilData unitval + +instance PlutusTx.FromData LockDatum where + {-# INLINEABLE fromBuiltinData #-} + fromBuiltinData bData = + let cons3 sgNft' entered' underlyingTn lst = + matchList + lst + ( pure LockDatum + <*> fromBuiltinData sgNft' + <*> fromBuiltinData entered' + <*> fromBuiltinData underlyingTn + ) + (\_ _ -> Nothing) + cons2 sgNft' entered lst = + matchList + lst + Nothing + (cons3 sgNft' entered) + cons1 sgNft lst = + matchList + lst + Nothing + (cons2 sgNft) + cons0 lst = + matchList + lst + Nothing + cons1 + matchCase constrIndex lst = + ifThenElse + (constrIndex `equalsInteger` 0) + (Hask.const (cons0 lst)) + (Hask.const Nothing) + unitval + in matchData' + bData + matchCase + (Hask.const Nothing) + (Hask.const Nothing) + (Hask.const Nothing) + (Hask.const Nothing) + +instance PlutusTx.UnsafeFromData LockDatum where + {-# INLINEABLE PlutusTx.unsafeFromBuiltinData #-} + unsafeFromBuiltinData bData = + let constr = unsafeDataAsConstr bData + constrIndex = Internal.fst constr + lst1 = Internal.snd constr + lst2 = Internal.tail lst1 + lst3 = Internal.tail lst2 + sgNft = Internal.head lst1 + entered = Internal.head lst2 + underlyingTn = Internal.head lst3 + val = + LockDatum + (PlutusTx.unsafeFromBuiltinData sgNft) + (PlutusTx.unsafeFromBuiltinData entered) + (PlutusTx.unsafeFromBuiltinData underlyingTn) + in ifThenElse + (constrIndex `equalsInteger` 0) + (Hask.const val) + (Hask.const (traceError reconstructCaseError)) + PlutusTx.Builtins.Internal.unitval + +instance Eq LockDatum where + {-# INLINEABLE (==) #-} + LockDatum a b c == LockDatum a' b' c' = a == a' && b == b' && c == c' + +newtype MarketplaceDatum = MarketplaceDatum {getMarketplaceDatum :: AssetClass} + deriving stock (Hask.Show, Generic, Hask.Eq, Hask.Ord) + deriving anyclass (FromJSON, ToJSON, ToSchema) + deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) + +class Hashable a where + hash :: a -> BuiltinByteString + +instance Hashable BuiltinByteString where + {-# INLINEABLE hash #-} + hash = blake2b_256 + +instance Hashable Natural where + {-# INLINEABLE hash #-} + hash = hash . toBin . fromEnum + where + {-# INLINEABLE toBin #-} + toBin :: Integer -> BuiltinByteString + toBin n = toBin' n mempty + where + toBin' n' rest + | n' < 256 = consByteString n' rest + | otherwise = toBin' (n' `divide` 256) (consByteString (n' `modulo` 256) rest) + +instance (Hashable a, Hashable b) => Hashable (a, b) where + hash (a, b) = hash (hash a <> hash b) + +deriving via BuiltinByteString instance Hashable ValidatorHash +deriving via BuiltinByteString instance Hashable PaymentPubKeyHash +deriving via BuiltinByteString instance Hashable TokenName +deriving via BuiltinByteString instance Hashable CurrencySymbol +deriving via (CurrencySymbol, TokenName) instance Hashable AssetClass + +instance Hashable NftId where + {-# INLINEABLE hash #-} + hash nft = + hash $ + mconcat + [ hash $ nftId'price nft + , hash $ nftId'owner nft + , hash $ nftId'collectionNftTn nft + ] diff --git a/mlabs/src/Mlabs/Emulator/App.hs b/mlabs/src/Mlabs/Emulator/App.hs new file mode 100644 index 000000000..1413f41d5 --- /dev/null +++ b/mlabs/src/Mlabs/Emulator/App.hs @@ -0,0 +1,88 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Lending app emulator +module Mlabs.Emulator.App ( + App (..), + runApp, + lookupAppWallet, + noErrors, + someErrors, + checkWallets, +) where + +import PlutusTx.Prelude +import Prelude qualified as Hask (Show, print, uncurry) + +import Control.Monad.State.Strict (foldM) +import Data.List (foldl') +import Data.Map.Strict qualified as M +import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, (@=?)) +import Text.Show.Pretty (pPrint) + +import Mlabs.Control.Monad.State (PlutusState, runStateT) +import Mlabs.Emulator.Blockchain (BchState (..), BchWallet, Resp, applyResp) +import Mlabs.Emulator.Script (Script, runScript) +import Mlabs.Emulator.Types (UserId) + +-- | Prototype application +data App st act = App + { -- | lending pool + app'st :: !st + , -- | error log + -- ^ it reports on which act and pool state error has happened + app'log :: ![(act, st, BuiltinByteString)] + , -- | current state of blockchain + app'wallets :: !BchState + } + +-- | Lookup state of the blockchain-wallet for a given user-id. +lookupAppWallet :: UserId -> App st act -> Maybe BchWallet +lookupAppWallet uid App {..} = case app'wallets of + BchState wals -> M.lookup uid wals + +{- | Runs application with the list of actions. + Returns final state of the application. +-} +runApp :: (act -> PlutusState st [Resp]) -> App st act -> Script act -> App st act +runApp react app acts = foldl' go app (runScript acts) + where + -- There are two possible sources of errors: + -- * we can not make transition to state (react produces Left) + -- * the transition produces action on blockchain that leads to negative balances (applyResp produces Left) + go (App lp errs wallets) act = case runStateT (react act) lp of + Right (resp, nextState) -> case foldM (flip applyResp) wallets resp of + Right nextWallets -> App nextState errs nextWallets + Left err -> App lp ((act, lp, err) : errs) wallets + Left err -> App lp ((act, lp, err) : errs) wallets + +--------------------------------------------------- +-- test functions + +noErrors :: (Hask.Show act, Hask.Show st) => App st act -> Assertion +noErrors app = case app'log app of + [] -> assertBool "no errors" True + xs -> do + mapM_ printLog xs + assertFailure "There are errors" + where + printLog (act, lp, msg) = do + pPrint act + pPrint lp + Hask.print msg + +someErrors :: App st act -> Assertion +someErrors = assertBool "Script fails" . not . null . app'log + +-- | Check that we have those wallets after script was run. +checkWallets :: [(UserId, BchWallet)] -> App st act -> Assertion +checkWallets wals app = mapM_ (Hask.uncurry $ hasWallet app) wals + +-- | Checks that application state contains concrete wallet for a given user id. +hasWallet :: App st act -> UserId -> BchWallet -> Assertion +hasWallet app uid wal = lookupAppWallet uid app @=? Just wal diff --git a/mlabs/src/Mlabs/Emulator/Blockchain.hs b/mlabs/src/Mlabs/Emulator/Blockchain.hs new file mode 100644 index 000000000..fab025d4b --- /dev/null +++ b/mlabs/src/Mlabs/Emulator/Blockchain.hs @@ -0,0 +1,122 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-specialize #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fobject-code #-} + +-- | Simple emulation ob blockchain state +module Mlabs.Emulator.Blockchain ( + BchState (..), + BchWallet (..), + defaultBchWallet, + Resp (..), + applyResp, + moveFromTo, + toConstraints, + updateRespValue, +) where + +import PlutusTx.Prelude hiding (fromMaybe, maybe) + +import Data.Map.Strict as M (Map, alterF, empty, toList) +import Data.Maybe (fromMaybe, maybe) +import Ledger.Constraints (mustMintValue, mustPayToPubKey) +import Plutus.Contract.StateMachine (TxConstraints, Void) +import Plutus.V1.Ledger.Value (Value, assetClassValue) +import Prelude qualified as Hask (Eq, Show) + +import Mlabs.Emulator.Types (Coin, UserId (..)) + +-- | Blockchain state is a set of wallets +newtype BchState = BchState (M.Map UserId BchWallet) + +-- | For simplicity wallet is a map of coins to balances. +newtype BchWallet = BchWallet (Map Coin Integer) + deriving newtype (Hask.Show, Hask.Eq) + +instance Eq BchWallet where + (BchWallet a) == (BchWallet b) = M.toList a == M.toList b + +-- | Default empty wallet +defaultBchWallet :: BchWallet +defaultBchWallet = BchWallet M.empty + +{- | We can give money to wallets and take it from them. + We can mint new aToken coins on lending platform and burn it. +-} +data Resp + = -- | move coins on wallet + Move + { move'addr :: UserId -- where move happens + , move'coin :: Coin -- on which value + , move'amount :: Integer -- how many to add (can be negative) + } + | -- | mint new coins for lending platform + Mint + { mint'coin :: Coin + , mint'amount :: Integer + } + | -- | burns coins for lending platform + Burn + { mint'coin :: Coin + , mint'amount :: Integer + } + deriving stock (Hask.Show) + +-- | Moves from first user to second user +moveFromTo :: UserId -> UserId -> Coin -> Integer -> [Resp] +moveFromTo from to coin amount = + [ Move from coin (negate amount) + , Move to coin amount + ] + +-- | Applies response to the blockchain state. +applyResp :: Resp -> BchState -> Either BuiltinByteString BchState +applyResp resp (BchState wallets) = fmap BchState $ case resp of + Move addr coin amount -> updateWallet addr coin amount wallets + Mint coin amount -> updateWallet Self coin amount wallets + Burn coin amount -> updateWallet Self coin (negate amount) wallets + where + updateWallet addr coin amt m = M.alterF (maybe (pure Nothing) (fmap Just . updateBalance coin amt)) addr m + + updateBalance :: Coin -> Integer -> BchWallet -> Either BuiltinByteString BchWallet + updateBalance coin amt (BchWallet bals) = fmap BchWallet $ M.alterF (upd amt) coin bals + + upd amt x + | res >= 0 = Right $ Just res + | otherwise = Left "Negative balance" + where + res = fromMaybe 0 x + amt + +--------------------------------------------------------------- + +{-# INLINEABLE toConstraints #-} +toConstraints :: Resp -> TxConstraints Void Void +toConstraints = \case + Move addr coin amount | amount > 0 -> case addr of + -- pays to lendex app + Self -> mempty -- we already check this constraint with StateMachine + -- pays to the user + UserId pkh -> mustPayToPubKey pkh (assetClassValue coin amount) + Mint coin amount -> mustMintValue (assetClassValue coin amount) + Burn coin amount -> mustMintValue (assetClassValue coin $ negate amount) + _ -> mempty + +{-# INLINEABLE updateRespValue #-} +updateRespValue :: [Resp] -> Value -> Value +updateRespValue rs val = foldMap toRespValue rs <> val + +{-# INLINEABLE toRespValue #-} +toRespValue :: Resp -> Value +toRespValue = \case + Move Self coin amount -> assetClassValue coin amount + Mint coin amount -> assetClassValue coin amount + Burn coin amount -> assetClassValue coin (negate amount) + _ -> mempty diff --git a/mlabs/src/Mlabs/Emulator/Scene.hs b/mlabs/src/Mlabs/Emulator/Scene.hs new file mode 100644 index 000000000..c24b2d673 --- /dev/null +++ b/mlabs/src/Mlabs/Emulator/Scene.hs @@ -0,0 +1,91 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Set of balances for tests +module Mlabs.Emulator.Scene ( + Scene (..), + owns, + appOwns, + appAddress, + checkScene, + coinDiff, +) where + +import PlutusTx.Prelude hiding (Monoid, Semigroup, mempty, (<>)) +import Prelude (Monoid, Semigroup, mempty, (<>)) + +import Control.Applicative (Alternative (..)) + +import Data.List qualified as L +import Data.Map qualified as M +import Plutus.Contract.Test ( + TracePredicate, + Wallet, + assertNoFailedTransactions, + valueAtAddress, + walletFundsChange, + (.&&.), + ) +import Plutus.V1.Ledger.Address (Address) +import Plutus.V1.Ledger.Value (Value) +import Plutus.V1.Ledger.Value qualified as Value + +import Mlabs.Lending.Logic.Types (Coin) + +{- | Scene is users with balances and value that is owned by application script. + It can be built with Monoid instance from parts with handy functions: + + 'owns', 'apOwns', 'appAddress' + + With monoid instance we can specify only differences between test stages + and then add them app with @<>@ to the initial state of the scene. +-} +data Scene = Scene + { -- | user balances + scene'users :: M.Map Wallet Value + , -- | application script balance + scene'appValue :: Value + , -- | address of the app + scene'appAddress :: Maybe Address + } + +instance Semigroup Scene where + Scene us1 e1 maddr1 <> Scene us2 e2 maddr2 = + Scene (M.unionWith (<>) us1 us2) (e1 <> e2) (maddr1 <|> maddr2) + +instance Monoid Scene where + mempty = Scene mempty mempty Nothing + +-- | Creates scene with single user in it that owns so many coins, app owns zero coins. +owns :: Wallet -> [(Coin, Integer)] -> Scene +owns wal ds = Scene {scene'users = M.singleton wal (coinDiff ds), scene'appValue = mempty, scene'appAddress = Nothing} + +-- | Creates scene with no users and app owns given amount of coins. +appOwns :: [(Coin, Integer)] -> Scene +appOwns v = Scene {scene'users = mempty, scene'appValue = coinDiff v, scene'appAddress = Nothing} + +-- | Creates scene with no users and app owns given amount of coins. +appAddress :: Address -> Scene +appAddress addr = Scene {scene'users = mempty, scene'appValue = mempty, scene'appAddress = Just addr} + +-- | Turns scene to plutus checks. Every user ownership turns into 'walletFundsChange' check. +checkScene :: Scene -> TracePredicate +checkScene Scene {..} = + withAddressCheck $ + concatPredicates + (uncurry walletFundsChange <$> M.toList scene'users) + .&&. assertNoFailedTransactions + where + withAddressCheck = maybe id (\addr -> (valueAtAddress addr (== scene'appValue) .&&.)) scene'appAddress + +-- | Converts list of coins to value. +coinDiff :: [(Coin, Integer)] -> Value +coinDiff = foldMap (uncurry Value.assetClassValue) + +concatPredicates :: [TracePredicate] -> TracePredicate +concatPredicates = L.foldl1' (.&&.) diff --git a/mlabs/src/Mlabs/Emulator/Script.hs b/mlabs/src/Mlabs/Emulator/Script.hs new file mode 100644 index 000000000..50c5ca3e4 --- /dev/null +++ b/mlabs/src/Mlabs/Emulator/Script.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Helper for testing logic of lending pool +module Mlabs.Emulator.Script ( + Script, + runScript, + getCurrentTime, + putAct, +) where + +import Prelude (Applicative (..), Monoid (..), Semigroup (..)) + +import Control.Monad.State.Strict qualified as Strict +import Data.Foldable (Foldable (toList)) +import Data.Monoid (Sum (..)) +import Data.Sequence as Seq (Seq, empty, singleton) +import PlutusTx.Prelude (Integer, ($), (.)) + +-- | Collects user actions and allocates timestamps +type Script act = ScriptM act () + +-- | Auto-allocation of timestamps, monadic interface for collection of actions +newtype ScriptM act a = Script (Strict.State (St act) a) + deriving newtype (Strict.Functor, Applicative, Strict.Monad, Strict.MonadState (St act)) + +-- | Script accumulator state. +data St act = St + { -- | acts so far + st'acts :: Seq act + , -- | current timestamp + st'time :: Sum Integer + } + +instance Semigroup (St a) where + St a1 t1 <> St a2 t2 = St (a1 <> a2) (t1 <> t2) + +instance Monoid (St a) where + mempty = St mempty mempty + +-- | Extract list of acts from the script +runScript :: Script act -> [act] +runScript (Script actions) = + toList $ st'acts $ Strict.execState actions (St empty 0) + +getCurrentTime :: ScriptM act Integer +getCurrentTime = Strict.gets (getSum . st'time) + +putAct :: act -> Script act +putAct act = + Strict.modify' (<> St (singleton act) (Sum 1)) diff --git a/mlabs/src/Mlabs/Emulator/Types.hs b/mlabs/src/Mlabs/Emulator/Types.hs new file mode 100644 index 000000000..8b9363470 --- /dev/null +++ b/mlabs/src/Mlabs/Emulator/Types.hs @@ -0,0 +1,49 @@ +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-specialize #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fobject-code #-} + +module Mlabs.Emulator.Types ( + UserId (..), + Coin, + adaCoin, + ownUserId, +) where + +import PlutusTx.Prelude + +import Data.Aeson (FromJSON, ToJSON) +import GHC.Generics (Generic) +import Ledger (PaymentPubKeyHash) +import Plutus.Contract (AsContractError, Contract, ownPaymentPubKeyHash) +import Plutus.V1.Ledger.Ada qualified as Ada +import Plutus.V1.Ledger.Value (AssetClass (..)) +import PlutusTx (unstableMakeIsData) +import Prelude qualified as Hask + +-- | Address of the wallet that can hold values of assets +data UserId + = UserId PaymentPubKeyHash -- user address + | Self -- addres of the lending platform + deriving stock (Hask.Show, Generic, Hask.Eq, Hask.Ord) + deriving anyclass (FromJSON, ToJSON) + +instance Eq UserId where + {-# INLINEABLE (==) #-} + Self == Self = True + UserId a == UserId b = a == b + _ == _ = False + +{-# INLINEABLE adaCoin #-} +adaCoin :: Coin +adaCoin = AssetClass (Ada.adaSymbol, Ada.adaToken) + +-- | Custom currency +type Coin = AssetClass + +PlutusTx.unstableMakeIsData ''UserId + +-- | Get user id of the wallet owner. +ownUserId :: AsContractError e => Contract w s e UserId +ownUserId = fmap UserId ownPaymentPubKeyHash diff --git a/mlabs/src/Mlabs/Governance/Contract.hs b/mlabs/src/Mlabs/Governance/Contract.hs new file mode 100644 index 000000000..e0eaf5665 --- /dev/null +++ b/mlabs/src/Mlabs/Governance/Contract.hs @@ -0,0 +1,7 @@ +-- | Re-export module +module Mlabs.Governance.Contract ( + module X, +) where + +import Mlabs.Governance.Contract.Api as X +import Mlabs.Governance.Contract.Server as X diff --git a/mlabs/src/Mlabs/Governance/Contract/Api.hs b/mlabs/src/Mlabs/Governance/Contract/Api.hs new file mode 100644 index 000000000..a6b5dc58b --- /dev/null +++ b/mlabs/src/Mlabs/Governance/Contract/Api.hs @@ -0,0 +1,80 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Contract API for the Governance application +module Mlabs.Governance.Contract.Api ( + Deposit (..), + Withdraw (..), + ProvideRewards (..), + QueryBalance (..), + GovernanceSchema, +) where + +import PlutusTx qualified +import PlutusTx.Prelude + +import GHC.Generics (Generic) + +-- import Numeric.Natural (Natural) +import Playground.Contract (FromJSON, ToJSON, ToSchema) +import Plutus.Contract (type (.\/)) +import Plutus.V1.Ledger.Crypto (PubKeyHash) +import Plutus.V1.Ledger.Value (Value) +import Prelude qualified as Hask + +import Mlabs.Plutus.Contract (Call, IsEndpoint (..)) + +-- TBD mixed withdraw/deposit endpoint + +-- since we have split of withdraw/deposit we might want to ensure that +-- the amounts have to be positive by construction, tbd (for now Natural has no ToSchema instance) +newtype Deposit = Deposit Integer + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving newtype (FromJSON, ToJSON) + deriving anyclass (ToSchema) + +PlutusTx.unstableMakeIsData ''Deposit + +newtype Withdraw = Withdraw [(PubKeyHash, Integer)] + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving newtype (FromJSON, ToJSON) + deriving anyclass (ToSchema) + +PlutusTx.unstableMakeIsData ''Withdraw + +newtype ProvideRewards = ProvideRewards Value + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving newtype (FromJSON, ToJSON) + deriving anyclass (ToSchema) + +newtype QueryBalance = QueryBalance PubKeyHash + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving newtype (FromJSON, ToJSON) + deriving anyclass (ToSchema) + +-- no need to split schemas +type GovernanceSchema = + Call Deposit + .\/ Call Withdraw + .\/ Call ProvideRewards + .\/ Call QueryBalance + +--- endpoint names + +instance IsEndpoint Deposit where + type EndpointSymbol Deposit = "deposit" + +instance IsEndpoint Withdraw where + type EndpointSymbol Withdraw = "withdraw" + +instance IsEndpoint ProvideRewards where + type EndpointSymbol ProvideRewards = "provide-rewards" + +instance IsEndpoint QueryBalance where + type EndpointSymbol QueryBalance = "query-balance" diff --git a/mlabs/src/Mlabs/Governance/Contract/Emulator/Client.hs b/mlabs/src/Mlabs/Governance/Contract/Emulator/Client.hs new file mode 100644 index 000000000..703e69596 --- /dev/null +++ b/mlabs/src/Mlabs/Governance/Contract/Emulator/Client.hs @@ -0,0 +1,43 @@ +-- | Client functions to test contracts in EmulatorTrace monad. +module Mlabs.Governance.Contract.Emulator.Client ( + callDeposit, + callWithdraw, + callProvideRewards, + queryBalance, +) where + +import Control.Monad (void) +import Plutus.Trace.Emulator (EmulatorTrace, activateContractWallet) +import PlutusTx.Prelude hiding (Semigroup (..), unless) +import Wallet.Emulator qualified as Emulator + +import Mlabs.Governance.Contract.Api qualified as Api +import Mlabs.Governance.Contract.Server qualified as Server +import Mlabs.Governance.Contract.Validation (AssetClassGov) +import Mlabs.Plutus.Contract (callEndpoint') + +-- imo it would be nicer if we were to take the type to be applied to callEndpoint' from the type sig itself + +-- | Deposits the specified amount of GOV into the governance contract +callDeposit :: AssetClassGov -> Emulator.Wallet -> Api.Deposit -> EmulatorTrace () +callDeposit gov wal depo = do + hdl <- activateContractWallet wal (Server.governanceEndpoints gov) + void $ callEndpoint' @Api.Deposit hdl depo + +-- | Withdraws the specified amount of GOV from the governance contract +callWithdraw :: AssetClassGov -> Emulator.Wallet -> Api.Withdraw -> EmulatorTrace () +callWithdraw gov wal withd = do + hdl <- activateContractWallet wal (Server.governanceEndpoints gov) + void $ callEndpoint' @Api.Withdraw hdl withd + +-- | Distributes the given Value amongst the xGOV holders +callProvideRewards :: AssetClassGov -> Emulator.Wallet -> Api.ProvideRewards -> EmulatorTrace () +callProvideRewards gov wal withd = do + hdl <- activateContractWallet wal (Server.governanceEndpoints gov) + void $ callEndpoint' @Api.ProvideRewards hdl withd + +-- | Queries the balance of a given PubKeyHash +queryBalance :: AssetClassGov -> Emulator.Wallet -> Api.QueryBalance -> EmulatorTrace () +queryBalance gov wal qb = do + hdl <- activateContractWallet wal (Server.governanceEndpoints gov) + void $ callEndpoint' @Api.QueryBalance hdl qb diff --git a/mlabs/src/Mlabs/Governance/Contract/Server.hs b/mlabs/src/Mlabs/Governance/Contract/Server.hs new file mode 100644 index 000000000..2f08593ca --- /dev/null +++ b/mlabs/src/Mlabs/Governance/Contract/Server.hs @@ -0,0 +1,197 @@ +{-# LANGUAGE OverloadedLists #-} + +-- | Server for governance application +module Mlabs.Governance.Contract.Server ( + GovernanceContract, + governanceEndpoints, +) where + +import PlutusTx.Prelude hiding (toList, uncurry) +import Prelude (String, show, uncurry) + +import Control.Lens ((^.), (^?)) +import Control.Monad (void) +import Data.List.Extra (maximumOn) +import Data.List.NonEmpty qualified as NE +import Data.Map qualified as Map +import Data.Semigroup (Last (..), sconcat) +import Data.Text (Text) +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash, unPaymentPubKeyHash)) +import Ledger.Constraints qualified as Constraints +import Ledger.Tx ( + ChainIndexTxOut, + TxOut (..), + TxOutRef, + ciTxOutDatum, + ciTxOutValue, + getCardanoTxId, + toTxOut, + txOutPubKey, + ) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Api ( + Datum (..), + Redeemer (..), + fromBuiltinData, + toBuiltinData, + ) +import Plutus.V1.Ledger.Value (Value (..), valueOf) +import PlutusTx.Ratio qualified as R +import Text.Printf (printf) + +import Mlabs.Governance.Contract.Api qualified as Api +import Mlabs.Governance.Contract.Validation (AssetClassGov (..), GovernanceDatum (..), GovernanceRedeemer (..)) +import Mlabs.Governance.Contract.Validation qualified as Validation +import Mlabs.Plutus.Contract (getEndpoint, selectForever) + +--import GHC.Base (Applicative(pure)) + +type GovernanceContract a = Contract.Contract (Maybe (Last Integer)) Api.GovernanceSchema Text a + +governanceEndpoints :: AssetClassGov -> GovernanceContract () +governanceEndpoints gov = + selectForever + [ getEndpoint @Api.Deposit $ deposit gov + , getEndpoint @Api.Withdraw $ withdraw gov + , getEndpoint @Api.ProvideRewards $ provideRewards gov + , getEndpoint @Api.QueryBalance $ queryBalance gov + ] + +--- actions + +deposit :: AssetClassGov -> Api.Deposit -> GovernanceContract () +deposit gov (Api.Deposit amnt) = do + ownPkh <- Contract.ownPaymentPubKeyHash + g <- findGovernance ownPkh gov + let (tx, lookups) = case g of + Just (datum, utxo, oref) -> + ( sconcat + [ Constraints.mustMintValue xGovValue + , Constraints.mustPayToTheScript datum $ Validation.govSingleton gov amnt <> (utxo ^. ciTxOutValue) + , Constraints.mustSpendScriptOutput oref (Redeemer . toBuiltinData $ GRDeposit amnt) + ] + , sconcat + [ Constraints.mintingPolicy $ Validation.xGovMintingPolicy gov + , Constraints.otherScript $ Validation.govValidator gov + , Constraints.typedValidatorLookups $ Validation.govInstance gov + , Constraints.unspentOutputs $ Map.singleton oref utxo + ] + ) + Nothing -> + let datum = GovernanceDatum (unPaymentPubKeyHash ownPkh) $ Validation.xGovCurrencySymbol gov + in ( sconcat + [ Constraints.mustMintValue xGovValue + , Constraints.mustPayToTheScript datum $ Validation.govSingleton gov amnt + ] + , sconcat + [ Constraints.mintingPolicy $ Validation.xGovMintingPolicy gov + , Constraints.otherScript $ Validation.govValidator gov + , Constraints.typedValidatorLookups $ Validation.govInstance gov + ] + ) + + xGovValue = Validation.xgovSingleton gov (unPaymentPubKeyHash ownPkh) amnt + + ledgerTx <- Contract.submitTxConstraintsWith @Validation.Governance lookups tx + void $ Contract.awaitTxConfirmed $ getCardanoTxId ledgerTx + Contract.logInfo @String $ printf "deposited %s GOV tokens" (show amnt) + +withdraw :: AssetClassGov -> Api.Withdraw -> GovernanceContract () +withdraw gov (Api.Withdraw assets) = do + ownPkh <- Contract.ownPaymentPubKeyHash + let trav f ~(x NE.:| xs) = (NE.:|) <$> f x <*> traverse f xs + -- for some reason NonEmpty doesn't have a Traversible instance in scope + (tx, lookups) <- fmap sconcat . flip trav (NE.fromList assets) $ \ac -> do + g <- findGovernance (PaymentPubKeyHash $ fst ac) gov + case g of + Nothing -> Contract.throwError "not found governance to withdraw from" + Just (datum, utxo, oref) -> + pure $ + let valxGov = Validation.xgovSingleton gov (fst ac) (snd ac) + valGov = Validation.govSingleton gov (snd ac) + scriptBalance = utxo ^. ciTxOutValue + in ( sconcat + [ Constraints.mustPayToTheScript datum $ scriptBalance - valGov + , Constraints.mustPayToPubKey ownPkh valGov + , Constraints.mustMintValue (negate valxGov) + , Constraints.mustSpendScriptOutput oref (Redeemer . toBuiltinData . GRWithdraw $ snd ac) + ] + , sconcat + [ Constraints.typedValidatorLookups $ Validation.govInstance gov + , Constraints.otherScript $ Validation.govValidator gov + , Constraints.mintingPolicy $ Validation.xGovMintingPolicy gov + , Constraints.unspentOutputs $ Map.singleton oref utxo + ] + ) + + ledgerTx <- Contract.submitTxConstraintsWith @Validation.Governance lookups tx + void $ Contract.awaitTxConfirmed $ getCardanoTxId ledgerTx + Contract.logInfo @String $ printf "withdrew %s GOV tokens" (show . sum $ map snd assets) + +-- TODO fix (works but transaction sizes are HUGE) +provideRewards :: AssetClassGov -> Api.ProvideRewards -> GovernanceContract () +provideRewards gov (Api.ProvideRewards val) = do + depositMap <- depositMapC + let -- annotates each depositor with the total percentage of GOV deposited to the contract + (total, props) = foldr (\(pkh, amm) (t, p) -> (amm + t, (pkh, R.reduce amm total) : p)) (0, mempty) depositMap + + dispatch = + map + ( \(pkh, prop) -> + case pkh of + Just pkh' -> Just (PaymentPubKeyHash pkh', Value $ fmap (round.(prop *).(`R.reduce` 1)) <$> getValue val) + Nothing -> Nothing + ) + props + + let aux = \case + Just x -> Just $ uncurry Constraints.mustPayToPubKey x + Nothing -> Nothing + tx <- maybe err pure $ foldMap aux dispatch + + let lookups = + sconcat + [ Constraints.otherScript $ Validation.govValidator gov + ] + + ledgerTx <- Contract.submitTxConstraintsWith @Validation.Governance lookups tx + void $ Contract.awaitTxConfirmed $ getCardanoTxId ledgerTx + Contract.logInfo @String $ printf "Provided rewards to all xGOV holders" + where + err = Contract.throwError "Could not find PublicKeyHash." + + govOf v = valueOf v (acGovCurrencySymbol gov) (acGovTokenName gov) + getPkh (_, o) = (,) ((txOutPubKey . toTxOut) o) (govOf . txOutValue . toTxOut $ o) + depositMapC = do + utxos <- fmap Map.toList . Contract.utxosAt $ Validation.govAddress gov + pure $ getPkh <$> utxos + +queryBalance :: AssetClassGov -> Api.QueryBalance -> GovernanceContract () +queryBalance gov (Api.QueryBalance pkh) = do + amm <- maybe 0 foo <$> findGovernance (PaymentPubKeyHash pkh) gov + Contract.tell . Just $ Last amm + where + foo (_, tx, _) = govOf $ tx ^. ciTxOutValue + govOf v = valueOf v (acGovCurrencySymbol gov) (acGovTokenName gov) + +--- util + +-- looks for governance, returns one with the biggest GOV value attached to it, if it exists +findGovernance :: + PaymentPubKeyHash -> + AssetClassGov -> + GovernanceContract (Maybe (Validation.GovernanceDatum, ChainIndexTxOut, TxOutRef)) +findGovernance pkh gov@AssetClassGov {..} = do + utxos <- Contract.utxosAt $ Validation.govAddress gov + case Map.toList utxos >>= foo of + [] -> pure Nothing + xs -> pure . Just $ maximumOn getVal xs + where + govOf v = valueOf v acGovCurrencySymbol acGovTokenName + + getVal (_, tx, _) = govOf $ tx ^. ciTxOutValue + foo (oref, o) = case o ^? ciTxOutDatum of + Just (Right (Datum e)) -> case fromBuiltinData e of + Just gd | gd == pkh -> [(GovernanceDatum (unPaymentPubKeyHash gd) acGovCurrencySymbol, o, oref)] + _ -> mempty + _ -> mempty diff --git a/mlabs/src/Mlabs/Governance/Contract/Simulator/Handler.hs b/mlabs/src/Mlabs/Governance/Contract/Simulator/Handler.hs new file mode 100644 index 000000000..89723e93f --- /dev/null +++ b/mlabs/src/Mlabs/Governance/Contract/Simulator/Handler.hs @@ -0,0 +1,124 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} + +module Mlabs.Governance.Contract.Simulator.Handler ( + GovernanceContracts (..), + handlers, + wallets, + govTokenName, + govAmount, +) where + +import Control.Monad (forM_, when) +import PlutusTx.Prelude +import Prelude (Show, show) + +import Mlabs.Governance.Contract.Api (GovernanceSchema) +import Mlabs.Governance.Contract.Server (governanceEndpoints) +import Mlabs.Governance.Contract.Validation (AssetClassGov (..)) + +import Data.Aeson (FromJSON, ToJSON) +import Data.Default (Default (def)) +import Data.Monoid (Last (..)) +import Data.OpenApi.Schema qualified as OpenApi +import Data.Text (Text, pack) +import GHC.Generics (Generic) + +import Control.Monad.Freer (interpret) +import Plutus.Contract (Contract, EmptySchema, awaitTxConfirmed, mapError, ownPaymentPubKeyHash, submitTx, tell) + +import Ledger (CurrencySymbol, getCardanoTxId) +import Ledger.Constraints (mustPayToPubKey) +import Mlabs.Utils.Wallet (walletFromNumber) +import Plutus.Contracts.Currency as Currency +import Plutus.V1.Ledger.Value qualified as Value +import Wallet.Emulator.Types (Wallet, mockWalletPaymentPubKeyHash) + +import Plutus.PAB.Core (EffectHandlers) +import Plutus.PAB.Effects.Contract.Builtin (Builtin, BuiltinHandler (contractHandler), HasDefinitions (..), SomeBuiltin (..), endpointsToSchemas, handleBuiltin) +import Plutus.PAB.Simulator () +import Plutus.PAB.Simulator as Simulator + +import Prettyprinter (Pretty (..), viaShow) + +-- FIXME this was passed as `BootstrapCfg` before update from calling side, +-- but now coz `bootstrapGovernance` moved here, had to hardcode them till can figure out better way +wallets :: [Wallet] +wallets = walletFromNumber <$> [1 .. 3] -- wallets participating, wallet #1 is admin +govTokenName :: Value.TokenName +govTokenName = "GOVToken" -- name of GOV token to be paid in exchange of xGOV tokens +govAmount :: Integer +govAmount = 100 + +-- data BootstrapCfg = BootstrapCfg +-- { wallets :: [Wallet] +-- , govTokenName :: TokenName +-- , govAmount :: Integer +-- } + +-- todo Additional Init contract TBD +data GovernanceContracts + = Bootstrap + | Governance AssetClassGov + deriving stock (Show, Generic) + deriving anyclass (FromJSON, ToJSON, OpenApi.ToSchema) + +instance Pretty GovernanceContracts where + pretty = viaShow + +type BootstrapContract = Contract (Last CurrencySymbol) EmptySchema Text () + +instance HasDefinitions GovernanceContracts where + -- FIXME couldn't understand what should be here, demo works both like this or [Bootstrap] + -- didn't try other variants + getDefinitions = [] + getSchema = \case + Bootstrap -> endpointsToSchemas @EmptySchema + Governance _ -> endpointsToSchemas @GovernanceSchema + getContract = \case + Bootstrap -> SomeBuiltin bootstrapGovernance + Governance params -> SomeBuiltin $ governanceEndpoints params + +handlers :: EffectHandlers (Builtin GovernanceContracts) (SimulatorState (Builtin GovernanceContracts)) +handlers = mkSimulatorHandlers def def handler + where + handler :: SimulatorContractHandler (Builtin GovernanceContracts) + handler = interpret (contractHandler handleBuiltin) + +-- FIXME before update it was possible to pass any initialization contract from Main to `handlers` +-- don't know how to achieve it now, had to move all config values +-- and initialization contract itself here for now just to make things work +-- maybe at least BootstrapCfg could passed from outside via `Bootstrap` +-- Bootstrap Contract which mints desired tokens +-- and distributes them ower wallets according to `BootstrapCfg` +bootstrapGovernance :: BootstrapContract +bootstrapGovernance = do + govCur <- mapError toText mintRequredTokens + let govCs = Currency.currencySymbol govCur + govPerWallet = Value.singleton govCs govTokenName govAmount + distributeGov govPerWallet + tell $ Last $ Just govCs + where + mintRequredTokens :: + Contract w EmptySchema Currency.CurrencyError Currency.OneShotCurrency + mintRequredTokens = do + ownPK <- ownPaymentPubKeyHash + Currency.mintContract ownPK [(govTokenName, govAmount * length wallets)] + + distributeGov govPerWallet = do + ownPK <- ownPaymentPubKeyHash + forM_ wallets $ \w -> do + let pkh = mockWalletPaymentPubKeyHash w + when (pkh /= ownPK) $ do + tx <- submitTx $ mustPayToPubKey pkh govPerWallet + awaitTxConfirmed $ getCardanoTxId tx + + toText = pack . show diff --git a/mlabs/src/Mlabs/Governance/Contract/Validation.hs b/mlabs/src/Mlabs/Governance/Contract/Validation.hs new file mode 100644 index 000000000..4f354b98a --- /dev/null +++ b/mlabs/src/Mlabs/Governance/Contract/Validation.hs @@ -0,0 +1,240 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-specialise #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fobject-code #-} + +-- | Validation, on-chain code for governance application +module Mlabs.Governance.Contract.Validation ( + govAddress, + govInstance, + govValidator, + govSingleton, + xgovSingleton, + xGovMintingPolicy, + xGovCurrencySymbol, + Governance, + GovernanceDatum (..), + GovernanceRedeemer (..), + AssetClassGov (..), +) where + +import PlutusTx.Prelude hiding (Semigroup (..), unless) +import Prelude qualified as Hask + +import Data.Bifunctor (first) +import Data.Coerce (coerce) +import GHC.Generics (Generic) + +import Data.OpenApi.Schema qualified as OpenApi + +import Playground.Contract (FromJSON, ToJSON) +import PlutusTx qualified +import PlutusTx.AssocMap qualified as AssocMap + +import Ledger hiding (after, before) +import Ledger.Typed.Scripts (wrapMintingPolicy) +import Ledger.Typed.Scripts.Validators qualified as Validators +import Plutus.V1.Ledger.Contexts qualified as Contexts +import Plutus.V1.Ledger.Credential (Credential (..)) +import Plutus.V1.Ledger.Value qualified as Value + +-- TODO: Once AssetClass has a ToSchema instance, change this to a newtype. +-- or not. this is fine really. +data AssetClassGov = AssetClassGov + { acGovCurrencySymbol :: !CurrencySymbol + , acGovTokenName :: !TokenName + } + deriving stock (Hask.Show, Hask.Eq, Generic) + deriving anyclass (ToJSON, FromJSON, OpenApi.ToSchema) + +instance Eq AssetClassGov where + {-# INLINEABLE (==) #-} + n1 == n2 = + acGovCurrencySymbol n1 == acGovCurrencySymbol n2 + && acGovTokenName n1 == acGovTokenName n2 + +PlutusTx.unstableMakeIsData ''AssetClassGov +PlutusTx.makeLift ''AssetClassGov + +data GovernanceRedeemer + = GRDeposit !Integer + | GRWithdraw !Integer + deriving stock (Hask.Show) + +instance Eq GovernanceRedeemer where + {-# INLINEABLE (==) #-} + (GRDeposit n1) == (GRDeposit n2) = n1 == n2 + (GRWithdraw n1) == (GRWithdraw n2) = n1 == n2 + _ == _ = False + +PlutusTx.unstableMakeIsData ''GovernanceRedeemer +PlutusTx.makeLift ''GovernanceRedeemer + +data GovernanceDatum = GovernanceDatum + { gdPubKeyHash :: !PubKeyHash + , gdxGovCurrencySymbol :: !CurrencySymbol + } + deriving stock (Hask.Show) + +PlutusTx.unstableMakeIsData ''GovernanceDatum +PlutusTx.makeLift ''GovernanceDatum + +data Governance +instance Validators.ValidatorTypes Governance where + type DatumType Governance = GovernanceDatum + type RedeemerType Governance = GovernanceRedeemer + +-- | governance validator +{-# INLINEABLE govValidator #-} + +mkValidator :: AssetClassGov -> GovernanceDatum -> GovernanceRedeemer -> ScriptContext -> Bool +mkValidator !gov !datum !redeemer !ctx = + traceIfFalse "incorrect value from redeemer" checkCorrectValue + && traceIfFalse "incorrect minting script involvenment" checkForging + && traceIfFalse "invalid datum update" checkCorrectDatumUpdate + where + !info = scriptContextTxInfo ctx + + ownInput :: Contexts.TxInInfo + !ownInput = case findOwnInput ctx of + Just !o -> o + Nothing -> traceError "no self in input" + + ownOutput :: Contexts.TxOut + !ownOutput = case Contexts.getContinuingOutputs ctx of + [!o] -> o -- this may crash here, may need to filter by pkh + _ -> traceError "expected exactly one continuing output" + + outputDatum :: GovernanceDatum + !outputDatum = case txOutDatumHash ownOutput of + Nothing -> traceError "no datum hash on governance" + Just h -> case findDatum h info of + Nothing -> traceError "no datum on governance" + Just (Datum d) -> case PlutusTx.fromBuiltinData d of + Nothing -> traceError "no datum parse" + Just gd -> gd + + valueIn :: Value + !valueIn = txOutValue $ txInInfoResolved ownInput + + valueOut :: Value + !valueOut = txOutValue ownOutput + + pkh :: PubKeyHash + pkh = gdPubKeyHash datum + + xGov :: CurrencySymbol + xGov = gdxGovCurrencySymbol datum + + --- checks + + checkForging :: Bool + !checkForging = case AssocMap.lookup xGov . Value.getValue $ txInfoMint info of + Nothing -> False + Just !mp -> case (redeemer, AssocMap.lookup (coerce pkh) mp) of + (GRDeposit !n, Just !m) -> n == m + (GRWithdraw !n, Just !m) -> n == negate m + _ -> False + + checkCorrectValue :: Bool + !checkCorrectValue = case redeemer of + GRDeposit !n -> n > 0 && valueIn + govSingleton gov n == valueOut + GRWithdraw !n -> n > 0 && valueIn - govSingleton gov n == valueOut + + checkCorrectDatumUpdate :: Bool + !checkCorrectDatumUpdate = + pkh == gdPubKeyHash outputDatum + && xGov == gdxGovCurrencySymbol outputDatum + +govInstance :: AssetClassGov -> Validators.TypedValidator Governance +govInstance gov = + Validators.mkTypedValidator @Governance + ( $$(PlutusTx.compile [||mkValidator||]) + `PlutusTx.applyCode` PlutusTx.liftCode gov + ) + $$(PlutusTx.compile [||wrap||]) + where + wrap = Validators.wrapValidator @GovernanceDatum @GovernanceRedeemer + +govValidator :: AssetClassGov -> Validator +govValidator = Validators.validatorScript . govInstance + +govAddress :: AssetClassGov -> Ledger.Address +govAddress = scriptAddress . govValidator + +{-# INLINEABLE govSingleton #-} +govSingleton :: AssetClassGov -> Integer -> Value +govSingleton AssetClassGov {..} = Value.singleton acGovCurrencySymbol acGovTokenName + +xgovSingleton :: AssetClassGov -> PubKeyHash -> Integer -> Value +xgovSingleton gov pkh = Value.singleton (xGovCurrencySymbol gov) (coerce pkh) + +-- xGOV minting policy +{-# INLINEABLE mkPolicy #-} +mkPolicy :: ValidatorHash -> AssetClassGov -> () -> ScriptContext -> Bool +mkPolicy vh AssetClassGov {..} _ !ctx = + traceIfFalse "attempt to mint unpaid-for xGOV" checkMintedSubsetGovDeposits + where + !info = scriptContextTxInfo ctx + + isGov (ScriptCredential v) = v == vh + isGov _ = False + + getGovernanceIn :: [TxOut] + !getGovernanceIn = filter (isGov . addressCredential . txOutAddress) . map txInInfoResolved $ txInfoInputs info + + getGovernanceOut :: [TxOut] + !getGovernanceOut = filter (isGov . addressCredential . txOutAddress) $ txInfoOutputs info + + -- how much GOV sits 'at every pkh' + pkhWithGov :: [TxOut] -> [(PubKeyHash, Integer)] + pkhWithGov !inout = inout >>= extractDatum + where + extractDatum !utxo = case txOutDatumHash utxo of + Nothing -> traceError "no datum hash on governance" + Just h -> case findDatum h info of + Nothing -> traceError "no datum on governance" + Just (Datum d) -> case PlutusTx.fromBuiltinData d of + Nothing -> traceError "no datum parse" + Just !gd -> case AssocMap.lookup acGovCurrencySymbol . Value.getValue $ txOutValue utxo of + Nothing -> [] -- just in case someone puts some other tokens in the script + Just !mp -> [(gdPubKeyHash gd, snd . head $ AssocMap.toList mp)] + + differenceGovDeposits :: [(PubKeyHash, Integer)] + !differenceGovDeposits = filter ((> 0) . snd) $ foldr foo [] (pkhWithGov getGovernanceOut) + where + !inMap = AssocMap.fromList $ pkhWithGov getGovernanceIn + + foo (pkh, !n) !xs = case AssocMap.lookup pkh inMap of + Nothing -> (pkh, n) : xs + Just !m -> (pkh, n - m) : xs + + mintedDeposit :: [(TokenName, Integer)] + mintedDeposit = case AssocMap.lookup (ownCurrencySymbol ctx) . Value.getValue $ txInfoMint info of + Nothing -> traceError "no self minting" + Just !mp -> filter ((> 0) . snd) $ AssocMap.toList mp + + -- checks + + -- mintedDeposit \subseteq differenceGovDeposits => minteddeposit == differencegovdeposits + checkMintedSubsetGovDeposits :: Bool + checkMintedSubsetGovDeposits = foldr memb True (map (first coerce) mintedDeposit) + where + memb !pair !b = (b &&) . foldr (||) False $ map (== pair) differenceGovDeposits + +-- yes, I've only done it ^this way so that it compiles + +xGovMintingPolicy :: AssetClassGov -> MintingPolicy +xGovMintingPolicy gov = + mkMintingPolicyScript $ + $$(PlutusTx.compile [||(wrapMintingPolicy .) . mkPolicy||]) + `PlutusTx.applyCode` PlutusTx.liftCode (validatorHash $ govValidator gov) + `PlutusTx.applyCode` PlutusTx.liftCode gov + +-- | takes in the GOV CurrencySymbol, returns the xGOV CurrencySymbol +xGovCurrencySymbol :: AssetClassGov -> CurrencySymbol +xGovCurrencySymbol = scriptCurrencySymbol . xGovMintingPolicy diff --git a/mlabs/src/Mlabs/Lending/Contract.hs b/mlabs/src/Mlabs/Lending/Contract.hs new file mode 100644 index 000000000..1007a3f78 --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Contract.hs @@ -0,0 +1,9 @@ +-- | Re-export module +module Mlabs.Lending.Contract ( + module X, +) where + +import Mlabs.Lending.Contract.Api as X +import Mlabs.Lending.Contract.Forge as X +import Mlabs.Lending.Contract.Server as X +import Mlabs.Lending.Contract.StateMachine as X diff --git a/mlabs/src/Mlabs/Lending/Contract/Api.hs b/mlabs/src/Mlabs/Lending/Contract/Api.hs new file mode 100644 index 000000000..336a838e1 --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Contract/Api.hs @@ -0,0 +1,325 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Contract API for Lendex application +module Mlabs.Lending.Contract.Api ( + -- * Actions + + -- ** User actions + Deposit (..), + Borrow (..), + Repay (..), + SwapBorrowRateModel (..), + AddCollateral (..), + RemoveCollateral (..), + Withdraw (..), + LiquidationCall (..), + InterestRateFlag (..), + toInterestRateFlag, + fromInterestRateFlag, + + -- ** Admin actions + AddReserve (..), + StartLendex (..), + + -- ** Query actions + QueryAllLendexes (..), + QuerySupportedCurrencies (..), + QueryCurrentBalance (..), + QueryInsolventAccounts (..), + + -- ** Price oracle actions + SetAssetPrice (..), + + -- ** Action conversions + IsUserAct (..), + IsPriceAct (..), + IsGovernAct (..), + IsQueryAct (..), + + -- * Schemas + UserSchema, + OracleSchema, + AdminSchema, + QuerySchema, +) where + +import PlutusTx.Prelude + +import GHC.Generics (Generic) +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash)) +import Playground.Contract (FromJSON, ToJSON, ToSchema) +import Plutus.Contract (type (.\/)) +import Plutus.V1.Ledger.Crypto (PubKeyHash) +import Prelude qualified as Hask (Eq, Show) + +import Mlabs.Lending.Logic.Types qualified as Types +import Mlabs.Plutus.Contract (Call, IsEndpoint (..)) + +----------------------------------------------------------------------- +-- lending pool actions + +-- user actions + +-- | Deposit funds to app +data Deposit = Deposit + { deposit'amount :: Integer + , deposit'asset :: Types.Coin + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +-- | Borrow funds. We have to allocate collateral to be able to borrow +data Borrow = Borrow + { borrow'amount :: Integer + , borrow'asset :: Types.Coin + , borrow'rate :: InterestRateFlag + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +-- | Repay part of the borrow +data Repay = Repay + { repay'amount :: Integer + , repay'asset :: Types.Coin + , repay'rate :: InterestRateFlag + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +-- | Swap borrow interest rate strategy (stable to variable) +data SwapBorrowRateModel = SwapBorrowRateModel + { swapRate'asset :: Types.Coin + , swapRate'rate :: InterestRateFlag + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +-- | Transfer portion of asset from the user's wallet to the contract, locked as the user's Collateral +data AddCollateral = AddCollateral + { -- | which Asset to use as collateral + addCollateral'asset :: Types.Coin + , -- | amount of Asset to take from Wallet and use as Collateral + addCollateral'amount :: Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +-- | Transfer portion of asset from locked user's Collateral to user's wallet +data RemoveCollateral = RemoveCollateral + { -- | which Asset to use as collateral or not + removeCollateral'asset :: Types.Coin + , -- | amount of Asset to remove from Collateral and put back to Wallet + removeCollateral'amount :: Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +-- | Withdraw funds from deposit +data Withdraw = Withdraw + { withdraw'amount :: Integer + , withdraw'asset :: Types.Coin + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +{- | Call to liquidate borrows that are unsafe due to health check + (see for description) +-} +data LiquidationCall = LiquidationCall + { -- | which collateral do we take for borrow repay + liquidationCall'collateral :: Types.Coin + , -- | identifier of the unhealthy borrow user + liquidationCall'debtUser :: PubKeyHash + , -- | identifier of the unhealthy borrow asset + liquidationCall'debtAsset :: Types.Coin + , -- | how much of the debt we cover + liquidationCall'debtToCover :: Integer + , -- | if true, the user receives the aTokens equivalent + -- of the purchased collateral. If false, the user receives + -- the underlying asset directly. + liquidationCall'receiveAToken :: Bool + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +-- deriving stock (Show, Generic, Hask.Eq) +-- deriving anyclass (FromJSON, ToJSON) + +-- admin actions + +-- | Adds new reserve +newtype AddReserve = AddReserve Types.CoinCfg + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +newtype StartLendex = StartLendex Types.StartParams + deriving newtype (Hask.Show, Generic, Hask.Eq, FromJSON, ToJSON, ToSchema) + +-- query actions + +newtype QueryAllLendexes = QueryAllLendexes Types.StartParams + deriving newtype (Hask.Show, Generic, Hask.Eq, FromJSON, ToJSON, ToSchema) + +newtype QuerySupportedCurrencies = QuerySupportedCurrencies () + deriving stock (Hask.Show, Generic) + deriving newtype (FromJSON, ToJSON, ToSchema) + +newtype QueryCurrentBalance = QueryCurrentBalance () + deriving stock (Hask.Show, Generic) + deriving newtype (FromJSON, ToJSON, ToSchema) + +newtype QueryInsolventAccounts = QueryInsolventAccounts () + deriving stock (Hask.Show, Generic) + deriving newtype (FromJSON, ToJSON, ToSchema) + +-- price oracle actions + +-- | Updates for the prices of the currencies on the markets +data SetAssetPrice = SetAssetPrice Types.Coin Rational + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +---------------------------------------------------------- +-- schemas + +-- | User actions +type UserSchema = + Call Deposit + .\/ Call Borrow + .\/ Call Repay + .\/ Call SwapBorrowRateModel + -- .\/ Call SetUserReserveAsCollateral + .\/ Call AddCollateral + .\/ Call RemoveCollateral + .\/ Call Withdraw + .\/ Call LiquidationCall + +-- | Oracle schema +type OracleSchema = + Call SetAssetPrice + +-- | Admin schema +type AdminSchema = + Call AddReserve + .\/ Call StartLendex + +-- | Query schema +type QuerySchema = + Call QueryAllLendexes + .\/ Call QuerySupportedCurrencies + .\/ Call QueryCurrentBalance + .\/ Call QueryInsolventAccounts + +---------------------------------------------------------- +-- proxy types for ToSchema instance + +{- | Interest rate flag. + + * 0 is stable rate + * everything else is variable rate +-} +newtype InterestRateFlag = InterestRateFlag Integer + deriving newtype (Hask.Show, Hask.Eq, FromJSON, ToJSON, ToSchema) + +fromInterestRateFlag :: InterestRateFlag -> Types.InterestRate +fromInterestRateFlag (InterestRateFlag n) + | n == 0 = Types.StableRate + | otherwise = Types.VariableRate + +toInterestRateFlag :: Types.InterestRate -> InterestRateFlag +toInterestRateFlag = + InterestRateFlag . \case + Types.StableRate -> 0 + Types.VariableRate -> 1 + +---------------------------------------------------------- +-- boilerplate to logic-act conversions + +class IsEndpoint a => IsUserAct a where + toUserAct :: a -> Types.UserAct + +class IsEndpoint a => IsPriceAct a where + toPriceAct :: a -> Types.PriceAct + +class IsEndpoint a => IsGovernAct a where + toGovernAct :: a -> Types.GovernAct + +class IsEndpoint a => IsQueryAct a where + toQueryAct :: a -> Types.QueryAct + +-- user acts + +instance IsUserAct Deposit where toUserAct Deposit {..} = Types.DepositAct deposit'amount deposit'asset +instance IsUserAct Borrow where toUserAct Borrow {..} = Types.BorrowAct borrow'amount borrow'asset (fromInterestRateFlag borrow'rate) +instance IsUserAct Repay where toUserAct Repay {..} = Types.RepayAct repay'amount repay'asset (fromInterestRateFlag repay'rate) +instance IsUserAct SwapBorrowRateModel where toUserAct SwapBorrowRateModel {..} = Types.SwapBorrowRateModelAct swapRate'asset (fromInterestRateFlag swapRate'rate) +instance IsUserAct AddCollateral where toUserAct AddCollateral {..} = Types.AddCollateralAct addCollateral'asset addCollateral'amount +instance IsUserAct RemoveCollateral where toUserAct RemoveCollateral {..} = Types.RemoveCollateralAct removeCollateral'asset removeCollateral'amount +instance IsUserAct Withdraw where toUserAct Withdraw {..} = Types.WithdrawAct withdraw'asset withdraw'amount +instance IsUserAct LiquidationCall where toUserAct LiquidationCall {..} = Types.LiquidationCallAct liquidationCall'collateral (Types.BadBorrow (Types.UserId $ PaymentPubKeyHash liquidationCall'debtUser) liquidationCall'debtAsset) liquidationCall'debtToCover liquidationCall'receiveAToken + +-- price acts + +instance IsPriceAct SetAssetPrice where toPriceAct (SetAssetPrice asset rate) = Types.SetAssetPriceAct asset rate + +-- govern acts + +instance IsGovernAct AddReserve where toGovernAct (AddReserve cfg) = Types.AddReserveAct cfg + +-- query acts + +instance IsQueryAct QueryCurrentBalance where toQueryAct (QueryCurrentBalance ()) = Types.QueryCurrentBalanceAct () +instance IsQueryAct QueryInsolventAccounts where toQueryAct (QueryInsolventAccounts ()) = Types.QueryInsolventAccountsAct () + +-- endpoint names + +instance IsEndpoint Deposit where + type EndpointSymbol Deposit = "deposit" + +instance IsEndpoint Borrow where + type EndpointSymbol Borrow = "borrow" + +instance IsEndpoint Repay where + type EndpointSymbol Repay = "repay" + +instance IsEndpoint SwapBorrowRateModel where + type EndpointSymbol SwapBorrowRateModel = "swap-borrow-rate-model" + +instance IsEndpoint AddCollateral where + type EndpointSymbol AddCollateral = "add-collateral" + +instance IsEndpoint RemoveCollateral where + type EndpointSymbol RemoveCollateral = "remove-collateral" + +instance IsEndpoint Withdraw where + type EndpointSymbol Withdraw = "withdraw" + +instance IsEndpoint LiquidationCall where + type EndpointSymbol LiquidationCall = "liquidation-call" + +instance IsEndpoint SetAssetPrice where + type EndpointSymbol SetAssetPrice = "set-asset-price" + +instance IsEndpoint AddReserve where + type EndpointSymbol AddReserve = "add-reserve" + +instance IsEndpoint StartLendex where + type EndpointSymbol StartLendex = "start-lendex" + +instance IsEndpoint QueryAllLendexes where + type EndpointSymbol QueryAllLendexes = "query-all-lendexes" + +instance IsEndpoint QuerySupportedCurrencies where + type EndpointSymbol QuerySupportedCurrencies = "query-supported-currencies" + +instance IsEndpoint QueryCurrentBalance where + type EndpointSymbol QueryCurrentBalance = "query-current-balance" + +instance IsEndpoint QueryInsolventAccounts where + type EndpointSymbol QueryInsolventAccounts = "query-insolvent-accounts" diff --git a/mlabs/src/Mlabs/Lending/Contract/Emulator/Client.hs b/mlabs/src/Mlabs/Lending/Contract/Emulator/Client.hs new file mode 100644 index 000000000..bf4ec944a --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Contract/Emulator/Client.hs @@ -0,0 +1,89 @@ +-- | Client functions to test contracts in EmulatorTrace monad. +module Mlabs.Lending.Contract.Emulator.Client ( + callUserAct, + callPriceAct, + callGovernAct, + callStartLendex, + callQueryAct, + queryAllLendexes, +) where + +import Prelude + +import Data.Functor (void) +import Data.Semigroup (Last (..)) +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash)) +import Plutus.Trace.Emulator (EmulatorRuntimeError (..), EmulatorTrace, activateContractWallet, callEndpoint, observableState, throwError) +import Plutus.V1.Ledger.Tx +import Wallet.Emulator qualified as Emulator + +import Mlabs.Lending.Contract.Api qualified as Api +import Mlabs.Lending.Contract.Server (adminEndpoints, oracleEndpoints, queryEndpoints, userEndpoints) +import Mlabs.Lending.Logic.Types qualified as Types +import Mlabs.Plutus.Contract (callEndpoint') + +--------------------------------------------------------- +-- call endpoints (for debug and testing) + +-- | Calls user act +callUserAct :: Types.LendexId -> Emulator.Wallet -> Types.UserAct -> EmulatorTrace () +callUserAct lid wal act = do + hdl <- activateContractWallet wal (userEndpoints lid) + void $ case act of + Types.DepositAct {..} -> callEndpoint' hdl $ Api.Deposit act'amount act'asset + Types.BorrowAct {..} -> callEndpoint' hdl $ Api.Borrow act'amount act'asset (Api.toInterestRateFlag act'rate) + Types.RepayAct {..} -> callEndpoint' hdl $ Api.Repay act'amount act'asset (Api.toInterestRateFlag act'rate) + Types.SwapBorrowRateModelAct {..} -> callEndpoint' hdl $ Api.SwapBorrowRateModel act'asset (Api.toInterestRateFlag act'rate) + Types.AddCollateralAct {..} -> callEndpoint' hdl $ Api.AddCollateral add'asset add'amount + Types.RemoveCollateralAct {..} -> callEndpoint' hdl $ Api.RemoveCollateral remove'asset remove'amount + Types.WithdrawAct {..} -> callEndpoint' hdl $ Api.Withdraw act'amount act'asset + Types.FlashLoanAct -> pure () -- todo + Types.LiquidationCallAct {..} -> + case act'debt of + Types.BadBorrow (Types.UserId (PaymentPubKeyHash pkh)) asset -> callEndpoint' hdl $ Api.LiquidationCall act'collateral pkh asset act'debtToCover act'receiveAToken + _ -> throwError $ GenericError "Bad borrow has wrong settings" + +-- | Calls query act +callQueryAct :: Types.LendexId -> Emulator.Wallet -> Types.QueryAct -> EmulatorTrace () +callQueryAct lid wal act = do + hdl <- activateContractWallet wal (queryEndpoints lid) + void $ case act of + Types.QueryCurrentBalanceAct () -> callEndpoint' hdl $ Api.QueryCurrentBalance () + Types.QueryInsolventAccountsAct () -> callEndpoint' hdl $ Api.QueryInsolventAccounts () + +-- | Calls price oracle act +callPriceAct :: Types.LendexId -> Emulator.Wallet -> Types.PriceAct -> EmulatorTrace () +callPriceAct lid wal act = do + hdl <- activateContractWallet wal (oracleEndpoints lid) + void $ case act of + Types.SetAssetPriceAct coin rate -> callEndpoint @"set-asset-price" hdl $ Api.SetAssetPrice coin rate + +-- | Calls govern act +callGovernAct :: Types.LendexId -> Emulator.Wallet -> Types.GovernAct -> EmulatorTrace () +callGovernAct lid wal act = do + hdl <- activateContractWallet wal (adminEndpoints lid) + void $ case act of + Types.AddReserveAct cfg -> callEndpoint @"add-reserve" hdl $ Api.AddReserve cfg + +-- | Calls initialisation of state for Lending pool +callStartLendex :: Types.LendexId -> Emulator.Wallet -> Api.StartLendex -> EmulatorTrace () +callStartLendex lid wal sl = do + hdl <- activateContractWallet wal (adminEndpoints lid) + void $ callEndpoint @"start-lendex" hdl sl + +-- todo: make a better query dispatch if the number of queries grows + +-- | Queries for all Lendexes started with given StartParams +queryAllLendexes :: + Types.LendexId -> + Emulator.Wallet -> + Api.QueryAllLendexes -> + EmulatorTrace [(Address, Types.LendingPool)] +queryAllLendexes lid wal spm = do + hdl <- activateContractWallet wal (queryEndpoints lid) + void $ callEndpoint @"query-all-lendexes" hdl spm + ls' <- observableState hdl + case ls' of + Just (Last (Types.QueryResAllLendexes ls)) -> + pure ls + _ -> throwError $ GenericError "Lendexes not found" diff --git a/mlabs/src/Mlabs/Lending/Contract/Forge.hs b/mlabs/src/Mlabs/Lending/Contract/Forge.hs new file mode 100644 index 000000000..b702adc3f --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Contract/Forge.hs @@ -0,0 +1,170 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE UndecidableInstances #-} + +module Mlabs.Lending.Contract.Forge ( + currencySymbol, + currencyPolicy, +) where + +import PlutusTx.Prelude + +import Control.Monad.State.Strict (evalStateT) +import Data.Either (fromRight) + +import Ledger (CurrencySymbol, PaymentPubKeyHash (PaymentPubKeyHash)) +import Ledger.Constraints (TxConstraints, checkScriptContext, mustPayToPubKey) +import Ledger.Contexts qualified as Contexts +import Ledger.Typed.Scripts as Scripts (MintingPolicy, wrapMintingPolicy) +import Plutus.V1.Ledger.Scripts as Scripts (Datum (getDatum), mkMintingPolicyScript) +import Plutus.V1.Ledger.Value qualified as Value +import PlutusTx (applyCode, compile, fromBuiltinData, liftCode) + +import Mlabs.Lending.Logic.State (getsWallet) + +import Mlabs.Lending.Logic.Types (LendingPool (lp'currency), Wallet (wallet'deposit)) +import Mlabs.Lending.Logic.Types qualified as Types + +data Input = Input + { input'lendexId :: !Types.LendexId + , input'state :: !Types.LendingPool + , input'value :: !Value.Value + } + +{-# INLINEABLE validate #-} + +{- | Validation script for minting policy. + + We allow user to forge coins just in two cases: + + * mint new aTokens in exchange for real tokens on deposit to lending app + * burn aTokens on withdraw from lending app + + For mint case we check that: + + * user deposit has grown properly on user's internal wallet for lending pool state + * user has paid enough real tokens to get aTokens + * script has paid enough aTokens to user in return + + For burn case we check that: + + * user deposit has diminished properly on user's internal wallet for lending pool state + * script has paid enough real tokens to the user in return + + Note that during burn user does not pay aTokens to the app they just get burned. + Only app pays to user in compensation for burn. +-} +validate :: Types.LendexId -> () -> Contexts.ScriptContext -> Bool +validate lendexId _ !ctx = case (getInState, getOutState) of + (Just !st1, Just !st2) -> + if hasLendexId st1 && hasLendexId st2 + then all (isValidForge st1 st2) $ Value.flattenValue $ Contexts.txInfoMint info + else traceIfFalse "Bad Lendex identifier" False + (Just _, Nothing) -> traceIfFalse "Failed to find LendingPool state in outputs" False + (Nothing, Just _) -> traceIfFalse "Failed to find LendingPool state in inputs" False + _ -> traceIfFalse "Failed to find TxOut with LendingPool state" False + where + hasLendexId !x = input'lendexId x == lendexId + + -- find datum of lending app state in the inputs + getInState = getStateForOuts (Contexts.txInInfoResolved <$> Contexts.txInfoInputs info) + + -- find datum of lending app state in the outputs + !getOutState = getStateForOuts $ Contexts.txInfoOutputs info + + getStateForOuts !outs = uniqueElement $ mapMaybe stateForTxOut outs + + stateForTxOut :: Contexts.TxOut -> Maybe Input + stateForTxOut !out = do + dHash <- Contexts.txOutDatumHash out + dat <- Scripts.getDatum <$> Contexts.findDatum dHash info + (lid, st) <- PlutusTx.fromBuiltinData dat + pure $ Input lid st (Contexts.txOutValue out) + + isValidForge :: Input -> Input -> (Value.CurrencySymbol, Value.TokenName, Integer) -> Bool + isValidForge !st1 !st2 (cur, token, !amount) = case getTokenCoin st1 st2 cur token of + Just !coin | amount >= 0 -> isValidMint st1 st2 coin aCoin amount + Just !coin -> isValidBurn st1 st2 coin aCoin (negate amount) + Nothing -> traceIfFalse "Minted token is not supported" False + where + !aCoin = Value.AssetClass (cur, token) + + getTokenCoin !st1 !st2 cur token + | isValidCurrency st1 st2 cur = Types.fromAToken (input'state st1) token + | otherwise = Nothing + + -- check if states are based on the same minting policy script + isValidCurrency !st1 !st2 cur = + cur == lp'currency (input'state st1) && cur == lp'currency (input'state st2) + + -- checks that user deposit becomes larger on given amount of minted tokens + -- and user pays given amount to the lending app. We go through the list of all signatures + -- to see if anyone acts as a user (satisfy constraints). + isValidMint (Input _ !st1 !stVal1) (Input _ !st2 !stVal2) !coin !aCoin !amount = + traceIfFalse "No user is allowed to mint" $ any checkUserMint users + where + checkUserMint uid = + checkUserDepositDiff uid + && checkUserPays + && checkScriptPays uid + + -- Check that user balance has grown on user inner wallet deposit + checkUserDepositDiff uid = + traceIfFalse "User deposit has not growed after Mint" $ + checkUserDepositDiffBy (\dep1 dep2 -> dep2 - dep1 == amount) st1 st2 coin uid + + -- Check that user payed value to script. + -- We check that state value became bigger after state transition. + checkUserPays = + traceIfFalse "User does not pay for Mint" $ + stVal2 == (stVal1 <> Value.assetClassValue coin amount) + + -- Check that user received aCoins + checkScriptPays uid = + traceIfFalse "User has not received aCoins for Mint" $ + checkScriptContext (mustPayToPubKey uid $ Value.assetClassValue aCoin amount :: TxConstraints () ()) ctx + + isValidBurn (Input _lendexId1 !st1 _stVal1) (Input _lendexId2 !st2 _stVal2) !coin _aCoin !amount = + traceIfFalse "No user is allowed to burn" $ any checkUserBurn users + where + checkUserBurn uid = + checkUserDepositDiff uid + && checkScriptPays uid + + -- Check that user balance has diminished on user inner wallet deposit + checkUserDepositDiff uid = + traceIfFalse "User deposit has not diminished after Burn" $ + checkUserDepositDiffBy (\dep1 dep2 -> dep1 - dep2 == amount) st1 st2 coin uid + + -- Check that user received coins + checkScriptPays uid = + traceIfFalse "User does not receive for Burn" $ + checkScriptContext (mustPayToPubKey uid $ Value.assetClassValue coin amount :: TxConstraints () ()) ctx + + -- check change of the user deposit for state prior to transition (st1) and after transition (st2) + checkUserDepositDiffBy !cond !st1 !st2 !coin uid = fromRight False $ do + !dep1 <- getDeposit uid coin st1 + !dep2 <- getDeposit uid coin st2 + pure $ cond dep1 dep2 + + getDeposit uid !coin !st = evalStateT (getsWallet (Types.UserId uid) coin wallet'deposit) st + + !users = PaymentPubKeyHash <$> Contexts.txInfoSignatories info + !info = Contexts.scriptContextTxInfo ctx + +------------------------------------------------------------------------------- + +currencyPolicy :: Types.LendexId -> MintingPolicy +currencyPolicy lid = + Scripts.mkMintingPolicyScript $ + $$(PlutusTx.compile [||Scripts.wrapMintingPolicy . validate||]) + `PlutusTx.applyCode` PlutusTx.liftCode lid + +currencySymbol :: Types.LendexId -> CurrencySymbol +currencySymbol lid = Contexts.scriptCurrencySymbol (currencyPolicy lid) diff --git a/mlabs/src/Mlabs/Lending/Contract/Server.hs b/mlabs/src/Mlabs/Lending/Contract/Server.hs new file mode 100644 index 000000000..70c95ffc7 --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Contract/Server.hs @@ -0,0 +1,251 @@ +{-# LANGUAGE NamedFieldPuns #-} + +-- | Server for lendex application +module Mlabs.Lending.Contract.Server ( + -- * Contract monads + UserContract, + OracleContract, + AdminContract, + + -- * Endpoints + userEndpoints, + oracleEndpoints, + adminEndpoints, + queryEndpoints, + + -- * Errors + StateMachine.LendexError, +) where + +import Control.Lens ((^.)) +import Control.Monad (guard) +import Control.Monad.State.Strict (runStateT) + +import Data.Bifunctor (second) +import Data.List.Extra (firstJust) +import Data.Map qualified as Map +import Data.Semigroup (Last (..)) + +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash)) +import Ledger.Constraints (mintingPolicy, mustIncludeDatum, ownPaymentPubKeyHash) +import Ledger.Tx (ChainIndexTxOut, ciTxOutAddress) + +import Plutus.Contract () +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Api (Datum (..)) +import Plutus.V1.Ledger.Slot (getSlot) +import Plutus.V1.Ledger.Tx + +import PlutusTx (FromData) +import PlutusTx.AssocMap qualified as M + +import Mlabs.Emulator.Types (UserId (..), ownUserId) +import Mlabs.Lending.Contract.Api qualified as Api +import Mlabs.Lending.Contract.Forge (currencyPolicy, currencySymbol) +import Mlabs.Lending.Contract.StateMachine qualified as StateMachine +import Mlabs.Lending.Logic.React qualified as React +import Mlabs.Lending.Logic.Types qualified as Types +import Mlabs.Plutus.Contract (getEndpoint, readChainIndexTxDatum, readDatum', selectForever) +import Plutus.Contract.Request qualified as Request + +import PlutusTx.Prelude +import Prelude qualified as Hask + +-- | User contract monad +type UserContract a = Contract.Contract () Api.UserSchema StateMachine.LendexError a + +-- | Oracle contract monad +type OracleContract a = Contract.Contract () Api.OracleSchema StateMachine.LendexError a + +-- | Admin contract monad +type AdminContract a = Contract.Contract () Api.AdminSchema StateMachine.LendexError a + +-- | Query contract monad +type QueryContract a = Contract.Contract QueryResult Api.QuerySchema StateMachine.LendexError a + +type QueryResult = Maybe (Last Types.QueryRes) + +---------------------------------------------------------- +-- endpoints + +-- | Endpoints for user +userEndpoints :: Types.LendexId -> UserContract () +userEndpoints lid = + selectForever + [ getEndpoint @Api.Deposit $ userAction lid + , getEndpoint @Api.Borrow $ userAction lid + , getEndpoint @Api.Repay $ userAction lid + , getEndpoint @Api.SwapBorrowRateModel $ userAction lid + , getEndpoint @Api.AddCollateral $ userAction lid + , getEndpoint @Api.RemoveCollateral $ userAction lid + , getEndpoint @Api.Withdraw $ userAction lid + , getEndpoint @Api.LiquidationCall $ userAction lid + ] + +-- TODO fix repetition +-- where +-- act :: Api.IsUserAct a => UserContract a -> UserContract () +-- act readInput = readInput >>= userAction lid + +-- | Endpoints for price oracle +oracleEndpoints :: Types.LendexId -> OracleContract () +oracleEndpoints lid = + selectForever + [ getEndpoint @Api.SetAssetPrice $ priceOracleAction lid + ] + +-- | Endpoints for admin +adminEndpoints :: Types.LendexId -> AdminContract () +adminEndpoints lid = do + Contract.toContract $ getEndpoint @Api.StartLendex $ startLendex lid + selectForever + [ getEndpoint @Api.AddReserve $ adminAction lid + ] + +{- | Endpoints for querrying Lendex state: + * `QueryAllLendexes` - returns a list of `LendingPool` data associated with each available lendes + * `QuerySupportedCurrencies` - returns the list of supported currencies (see `SupportedCurrency`) for current `LendingPool` + * `QuerryCurrentBalance` - returns a list of all the users, together with their current balances. +-} +queryEndpoints :: Types.LendexId -> QueryContract () +queryEndpoints lid = + selectForever + [ getEndpoint @Api.QueryAllLendexes $ queryAllLendexes lid + , getEndpoint @Api.QuerySupportedCurrencies $ \_ -> querySupportedCurrencies lid + , getEndpoint @Api.QueryCurrentBalance $ queryAction lid + , getEndpoint @Api.QueryInsolventAccounts $ queryAction lid + ] + +-- user actions + +userAction :: Api.IsUserAct a => Types.LendexId -> a -> UserContract () +userAction lid input = do + pkh <- Contract.ownPaymentPubKeyHash + act <- getUserAct input + inputDatum <- findInputStateDatum lid + let lookups = + mintingPolicy (currencyPolicy lid) + Hask.<> ownPaymentPubKeyHash pkh + constraints = mustIncludeDatum inputDatum + StateMachine.runStepWith lid act lookups constraints + +-- Oracle actions + +priceOracleAction :: Api.IsPriceAct a => Types.LendexId -> a -> OracleContract () +priceOracleAction lid input = StateMachine.runStep lid =<< getPriceAct input + +-- Admin actions + +adminAction :: Api.IsGovernAct a => Types.LendexId -> a -> AdminContract () +adminAction lid input = StateMachine.runStep lid =<< getGovernAct input + +startLendex :: Types.LendexId -> Api.StartLendex -> AdminContract () +startLendex lid (Api.StartLendex Types.StartParams {..}) = + StateMachine.runInitialise lid (Types.initLendingPool (currencySymbol lid) sp'coins (fmap (Types.UserId . PaymentPubKeyHash) sp'admins) (fmap (Types.UserId . PaymentPubKeyHash) sp'oracles)) sp'initValue + +-- Query actions + +queryAction :: Api.IsQueryAct a => Types.LendexId -> a -> QueryContract () +queryAction lid input = do + (_, pool) <- findDatum lid :: QueryContract (Types.LendexId, Types.LendingPool) + qAction pool =<< getQueryAct input + where + qAction :: Types.LendingPool -> Types.Act -> QueryContract () + qAction pool act = Contract.tell $ buildLog pool act + + -- Builds the Log by running a State Machine + buildLog :: Types.LendingPool -> Types.Act -> QueryResult + buildLog pool action = either (const Nothing) fst $ runStateT (React.qReact action) pool + +queryAllLendexes :: Types.LendexId -> Api.QueryAllLendexes -> QueryContract () +queryAllLendexes lid (Api.QueryAllLendexes spm) = do + utxos <- Contract.utxosAt $ StateMachine.lendexAddress lid + Contract.tell . Just . Last . Types.QueryResAllLendexes . mapMaybe f . Map.elems $ utxos + pure () + where + startedWith :: Types.LendingPool -> Types.StartParams -> Maybe Types.LendingPool + startedWith lp@Types.LendingPool {..} Types.StartParams {..} = do + guard (map (UserId . PaymentPubKeyHash) sp'admins == lp'admins) + guard (map (UserId . PaymentPubKeyHash) sp'oracles == lp'trustedOracles) + -- unsure if we can check that the tokens in StartParams are still being dealt in + -- there is no 100% certainty since AddReserve can add new Coin types + -- todo: we could check that the Coins is SartParams are a subset of the ones being dealt in now? + pure lp + + f :: ChainIndexTxOut -> Maybe (Address, Types.LendingPool) + f o = do + let add = o ^. ciTxOutAddress + (dat :: (Types.LendexId, Types.LendingPool)) <- readDatum' o + lp <- startedWith (snd dat) spm + pure (add, lp) + +querySupportedCurrencies :: Types.LendexId -> QueryContract () +querySupportedCurrencies lid = do + (_, pool) <- findInputStateData lid :: QueryContract (Types.LendexId, Types.LendingPool) + tellResult . getSupportedCurrencies $ pool + where + getSupportedCurrencies :: Types.LendingPool -> [Types.SupportedCurrency] + getSupportedCurrencies Types.LendingPool {lp'reserves} = + fmap toSupportedCurrency (M.toList lp'reserves) + + toSupportedCurrency (coin, Types.Reserve {reserve'aToken, reserve'rate}) = + Types.SupportedCurrency coin reserve'aToken reserve'rate + + tellResult = Contract.tell . Just . Last . Types.QueryResSupportedCurrencies + +---------------------------------------------------------- +-- to act conversion + +-- | Converts endpoint inputs to logic actions +getUserAct :: Api.IsUserAct a => a -> UserContract Types.Act +getUserAct act = do + uid <- ownUserId + t <- getCurrentTime + pure $ Types.UserAct t uid $ Api.toUserAct act + +-- | Converts endpoint inputs to logic actions +getPriceAct :: Api.IsPriceAct a => a -> OracleContract Types.Act +getPriceAct act = do + uid <- ownUserId + t <- getCurrentTime + pure $ Types.PriceAct t uid $ Api.toPriceAct act + +getGovernAct :: Api.IsGovernAct a => a -> AdminContract Types.Act +getGovernAct act = do + uid <- ownUserId + pure $ Types.GovernAct uid $ Api.toGovernAct act + +getQueryAct :: Api.IsQueryAct a => a -> QueryContract Types.Act +getQueryAct act = do + uid <- ownUserId + t <- getCurrentTime + pure $ Types.QueryAct uid t $ Api.toQueryAct act + +getCurrentTime :: Contract.AsContractError e => Contract.Contract w s e Integer +getCurrentTime = getSlot <$> Contract.currentSlot + +---------------------------------------------------------- + +findInputStateDatum :: Types.LendexId -> UserContract Datum +findInputStateDatum = findInputStateData + +findInputStateData :: FromData d => Types.LendexId -> Contract.Contract w s StateMachine.LendexError d +findInputStateData lid = do + txOuts <- Map.elems <$> Contract.utxosAt (StateMachine.lendexAddress lid) + maybe err pure $ firstJust readDatum' txOuts + where + err = Contract.throwError $ StateMachine.toLendexError "Can not find Lending app instance" + +-- | todo: add a unique NFT to distinguish between utxos / review logic. +findDatum :: FromData d => Types.LendexId -> Contract.Contract w s StateMachine.LendexError (Types.LendexId, d) +findDatum lid = do + txOuts <- filterTxOuts . Map.toList <$> Request.utxosTxOutTxAt (StateMachine.lendexAddress lid) + case txOuts of + [(_, [x])] -> maybe err return $ (lid,) <$> x -- only passes if there is only 1 datum instance. + _ -> err + where + err = Contract.throwError . StateMachine.toLendexError $ "Cannot establish correct Lending app instance." + filterTxOuts = filter ((== 1) . length . filter isNotNothing . snd) . fmap (second $ readChainIndexTxDatum . snd) + isNotNothing = \case + Nothing -> False + Just _ -> True diff --git a/mlabs/src/Mlabs/Lending/Contract/Simulator/Handler.hs b/mlabs/src/Mlabs/Lending/Contract/Simulator/Handler.hs new file mode 100644 index 000000000..a5d693d54 --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Contract/Simulator/Handler.hs @@ -0,0 +1,121 @@ +-- | Handlers for PAB simulator +module Mlabs.Lending.Contract.Simulator.Handler ( + Sim, + LendexContracts (..), + InitContract, + runSimulator, +) where + +import Prelude + +-- handler related imports commented out with `-- !` to disable compilation warnings +-- ! import Control.Monad.Freer ( +-- Eff, +-- Member, +-- interpret, +-- type (~>), +-- ) +-- ! import Control.Monad.Freer.Error (Error) +-- ! import Control.Monad.Freer.Extras.Log (LogMsg) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import Data.Aeson (FromJSON, ToJSON) + +-- ! import Data.Default (Default (def)) +import Data.Functor (void) +import Data.Monoid (Last) +import Data.OpenApi.Schema qualified as OpenApi +import GHC.Generics (Generic) +import Plutus.Contract (Contract, EmptySchema) +import Prettyprinter (Pretty (..), viaShow) + +-- ! import Plutus.PAB.Effects.Contract (ContractEffect (..)) +import Plutus.PAB.Effects.Contract.Builtin ( + Builtin, + -- ! , SomeBuiltin (..) + ) + +-- ! import Plutus.PAB.Effects.Contract.Builtin qualified as Builtin +-- ! import Plutus.PAB.Monitoring.PABLogMsg (PABMultiAgentMsg (..)) +import Plutus.PAB.Simulator ( + Simulation, + SimulatorEffectHandlers, + ) +import Plutus.PAB.Simulator qualified as Simulator + +-- ! import Plutus.PAB.Types (PABError (..)) +import Plutus.PAB.Webserver.Server qualified as PAB.Server +import Plutus.V1.Ledger.Value (CurrencySymbol) + +-- ! import Mlabs.Lending.Contract.Api qualified as Api +import Mlabs.Lending.Contract.Server qualified as Server +import Mlabs.Lending.Logic.Types (LendexId) + +-- | Shortcut for Simulator monad for NFT case +type Sim a = Simulation (Builtin LendexContracts) a + +-- | Lendex schemas +data LendexContracts + = -- | init wallets + Init + | -- | we read Lendex identifier and instantiate schema for the user actions + User + | -- | price oracle actions + Oracle + | -- | govern actions + Admin + | -- | Query actions + Query + deriving stock (Show, Generic) + deriving anyclass (FromJSON, ToJSON, OpenApi.ToSchema) + +instance Pretty LendexContracts where + pretty = viaShow + +type InitContract = Contract (Last CurrencySymbol) EmptySchema Server.LendexError () + +-- FIXME +-- handleLendexContracts lendexId initHandler = +-- handleLendexContracts :: +-- ( Member (Error PABError) effs +-- , Member (LogMsg (PABMultiAgentMsg (Builtin LendexContracts))) effs +-- ) => +-- LendexId -> +-- InitContract -> +-- ContractEffect (Builtin LendexContracts) ~> Eff effs +-- handleLendexContracts lendexId initHandler = +-- handleLendexContracts lendexId initHandler = +-- Builtin.handleBuiltin getSchema getContract +-- where +-- getSchema = \case +-- Init -> Builtin.endpointsToSchemas @EmptySchema +-- User -> Builtin.endpointsToSchemas @Api.UserSchema +-- Oracle -> Builtin.endpointsToSchemas @Api.OracleSchema +-- Admin -> Builtin.endpointsToSchemas @Api.AdminSchema +-- Query -> Builtin.endpointsToSchemas @Api.QuerySchema +-- getContract = \case +-- Init -> SomeBuiltin initHandler +-- User -> SomeBuiltin $ Server.userEndpoints lendexId +-- Oracle -> SomeBuiltin $ Server.oracleEndpoints lendexId +-- Admin -> SomeBuiltin $ Server.adminEndpoints lendexId +-- Query -> SomeBuiltin $ Server.queryEndpoints lendexId + +-- FIXME +handlers :: LendexId -> InitContract -> SimulatorEffectHandlers (Builtin LendexContracts) +handlers = error "Fix required after Plutus update" + +-- handlers lid initContract = +-- Simulator.mkSimulatorHandlers @(Builtin LendexContracts) def [] $ +-- interpret (handleLendexContracts lid initContract) + +-- | Runs simulator for Lendex +runSimulator :: LendexId -> InitContract -> Sim () -> IO () +runSimulator lid initContract = withSimulator (handlers lid initContract) + +withSimulator :: Simulator.SimulatorEffectHandlers (Builtin LendexContracts) -> Simulation (Builtin LendexContracts) () -> IO () +withSimulator hs act = void $ + Simulator.runSimulationWith hs $ do + Simulator.logString @(Builtin LendexContracts) "Starting PAB webserver. Press enter to exit." + shutdown <- PAB.Server.startServerDebug + void act + void $ liftIO getLine + shutdown diff --git a/mlabs/src/Mlabs/Lending/Contract/StateMachine.hs b/mlabs/src/Mlabs/Lending/Contract/StateMachine.hs new file mode 100644 index 000000000..25d83ec5b --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Contract/StateMachine.hs @@ -0,0 +1,142 @@ +{-# LANGUAGE BangPatterns #-} + +-- | State machine and binding of transitions to Plutus for lending app +module Mlabs.Lending.Contract.StateMachine ( + Lendex, + LendexError, + toLendexError, + lendexAddress, + runStep, + runStepWith, + runInitialise, +) where + +import Ledger.TimeSlot qualified as TimeSlot (posixTimeRangeToContainedSlotRange) +import PlutusTx.Prelude hiding (Applicative (..), Monoid (..), Semigroup (..), check) +import PlutusTx.Prelude qualified as Plutus +import Prelude qualified as Hask (String) + +import Control.Monad.State.Strict (runStateT) +import Data.Default (Default (def)) +import Data.Functor (void) +import Data.String (IsString (fromString)) +import Ledger qualified +import Ledger.Constraints (ScriptLookups, TxConstraints, mustBeSignedBy) +import Ledger.Typed.Scripts.Validators qualified as Validators +import Plutus.Contract qualified as Contract +import Plutus.Contract.StateMachine qualified as SM +import PlutusTx qualified + +import Mlabs.Emulator.Blockchain (toConstraints, updateRespValue) +import Mlabs.Lending.Logic.React (react) +import Mlabs.Lending.Logic.Types qualified as Types + +type Lendex = SM.StateMachine (Types.LendexId, Types.LendingPool) Types.Act + +-- | Error type +type LendexError = SM.SMContractError + +toLendexError :: Hask.String -> LendexError +toLendexError = SM.SMCContractError . fromString + +{-# INLINEABLE machine #-} +machine :: Types.LendexId -> Lendex +machine lid = + (SM.mkStateMachine Nothing (transition lid) isFinal) + { SM.smCheck = checkTimestamp + } + where + !isFinal = const False + + checkTimestamp _ !input !ctx = maybe True check $ getInputTime input + where + check !t = + Ledger.Slot t + `Ledger.member` TimeSlot.posixTimeRangeToContainedSlotRange def range + !range = Ledger.txInfoValidRange $ Ledger.scriptContextTxInfo ctx + + !getInputTime = \case + Types.UserAct time _ _ -> Just time + Types.PriceAct time _ _ -> Just time + _ -> Nothing + +{-# INLINEABLE mkValidator #-} +mkValidator :: Types.LendexId -> Validators.ValidatorType Lendex +mkValidator lid = SM.mkValidator (machine lid) + +client :: Types.LendexId -> SM.StateMachineClient (Types.LendexId, Types.LendingPool) Types.Act +client lid = SM.mkStateMachineClient $ SM.StateMachineInstance (machine lid) (scriptInstance lid) + +lendexValidatorHash :: Types.LendexId -> Ledger.ValidatorHash +lendexValidatorHash lid = Validators.validatorHash (scriptInstance lid) + +lendexAddress :: Types.LendexId -> Ledger.Address +lendexAddress lid = Ledger.scriptHashAddress (lendexValidatorHash lid) + +scriptInstance :: Types.LendexId -> Validators.TypedValidator Lendex +scriptInstance lid = + Validators.mkTypedValidator @Lendex + ( $$(PlutusTx.compile [||mkValidator||]) + `PlutusTx.applyCode` PlutusTx.liftCode lid + ) + $$(PlutusTx.compile [||wrap||]) + where + wrap = Validators.wrapValidator + +{-# INLINEABLE transition #-} +transition :: + Types.LendexId -> + SM.State (Types.LendexId, Types.LendingPool) -> + Types.Act -> + Maybe (SM.TxConstraints SM.Void SM.Void, SM.State (Types.LendexId, Types.LendingPool)) +transition lid SM.State {stateData = !oldData, stateValue = !oldValue} input + | lid == inputLid = case runStateT (react input) (snd oldData) of + Left _err -> Nothing + Right (!resps, !newData) -> + Just + ( foldMap toConstraints resps Plutus.<> ctxConstraints + , SM.State + { stateData = (lid, newData) + , stateValue = updateRespValue resps oldValue + } + ) + | otherwise = Nothing + where + inputLid = fst oldData + + -- we check that user indeed signed the transaction with his own key + !ctxConstraints = maybe Plutus.mempty mustBeSignedBy userId + + !userId = case input of + Types.UserAct _ (Types.UserId uid) _ -> Just uid + _ -> Nothing + +---------------------------------------------------------------------- +-- specific versions of SM-functions + +runStep :: + forall w e schema. + SM.AsSMContractError e => + Types.LendexId -> + Types.Act -> + Contract.Contract w schema e () +runStep lid act = void $ SM.runStep (client lid) act + +runStepWith :: + forall w e schema. + SM.AsSMContractError e => + Types.LendexId -> + Types.Act -> + ScriptLookups Lendex -> + TxConstraints (Validators.RedeemerType Lendex) (Validators.DatumType Lendex) -> + Contract.Contract w schema e () +runStepWith lid act lookups constraints = void $ SM.runStepWith lookups constraints (client lid) act + +runInitialise :: + forall w e schema. + SM.AsSMContractError e => + Types.LendexId -> + Types.LendingPool -> + Ledger.Value -> + Contract.Contract w schema e () +runInitialise lid lendingPool val = void $ SM.runInitialise (client lid) (lid, lendingPool) val diff --git a/mlabs/src/Mlabs/Lending/Contract/Utils.hs b/mlabs/src/Mlabs/Lending/Contract/Utils.hs new file mode 100644 index 000000000..a8a36bfeb --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Contract/Utils.hs @@ -0,0 +1,7 @@ +{-# OPTIONS_GHC -fno-specialize #-} + +module Mlabs.Lending.Contract.Utils where + +import Ledger hiding (singleton) +import PlutusTx +import Prelude (Maybe (..), ($)) diff --git a/mlabs/src/Mlabs/Lending/Logic/App.hs b/mlabs/src/Mlabs/Lending/Logic/App.hs new file mode 100644 index 000000000..85cf8c6bf --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Logic/App.hs @@ -0,0 +1,147 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Inits logic test suite app emulator +module Mlabs.Lending.Logic.App ( + -- * Application + LendingApp, + runLendingApp, + initApp, + AppConfig (..), + defaultAppConfig, + toCoin, + + -- * Script actions + Script, + userAct, + priceAct, + governAct, + queryAct, +) where + +import PlutusTx.Prelude +import Prelude qualified as Hask (uncurry) + +import Data.Map.Strict qualified as M +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash)) +import Plutus.V1.Ledger.Value qualified as Value +import PlutusTx.AssocMap qualified as AM + +import Mlabs.Emulator.App (App (..), runApp) +import Mlabs.Emulator.Blockchain (BchState (BchState), BchWallet (..), defaultBchWallet) +import Mlabs.Emulator.Script qualified as Script +import Mlabs.Lending.Logic.React (react) +import Mlabs.Lending.Logic.Types qualified as Types +import PlutusTx.Ratio qualified as R + +type LendingApp = App Types.LendingPool Types.Act + +runLendingApp :: AppConfig -> Script -> LendingApp +runLendingApp cfg = runApp react (initApp cfg) + +-- Configuration parameters for app. +data AppConfig = AppConfig + { -- | coins with ratios to base currencies for each reserve + appConfig'reserves :: [Types.CoinCfg] + , -- | initial set of users with their wallets on blockchain + -- the wallet for lending app wil be created automatically. + -- no need to include it here + appConfig'users :: [(Types.UserId, BchWallet)] + , -- | lending app main currency symbol + appConfig'currencySymbol :: Value.CurrencySymbol + , -- | users that can do govern actions + appConfig'admins :: [Types.UserId] + , -- | users that can submit price changes + appConfig'oracles :: [Types.UserId] + } + +-- | App is initialised with list of coins and their rates (value relative to base currency, ada for us) +initApp :: AppConfig -> LendingApp +initApp AppConfig {..} = + App + { app'st = + Types.LendingPool + { lp'reserves = AM.fromList (fmap (\x -> (Types.coinCfg'coin x, Types.initReserve x)) appConfig'reserves) + , lp'users = AM.empty + , lp'currency = appConfig'currencySymbol + , lp'coinMap = coinMap + , lp'healthReport = AM.empty + , lp'admins = appConfig'admins + , lp'trustedOracles = appConfig'oracles + } + , app'log = [] + , app'wallets = BchState $ M.fromList $ (Types.Self, defaultBchWallet) : appConfig'users + } + where + coinMap = + AM.fromList + . fmap + (\Types.CoinCfg {..} -> (coinCfg'aToken, coinCfg'coin)) + $ appConfig'reserves + +{- | Default application. + It allocates three users and three reserves for Dollars, Euros and Liras. + Each user has 100 units of only one currency. User 1 has dollars, user 2 has euros amd user 3 has liras. +-} +defaultAppConfig :: AppConfig +defaultAppConfig = AppConfig reserves users curSym admins oracles + where + admins = [user1] + oracles = [user1] + user1 = Types.UserId $ PaymentPubKeyHash "1" -- only user 1 can set the price and be admin + curSym = Value.currencySymbol "lending-app" + userNames = ["1", "2", "3"] + coinNames = ["Dollar", "Euro", "Lira"] + + reserves = + fmap + ( \name -> + Types.CoinCfg + { coinCfg'coin = toCoin name + , coinCfg'rate = R.fromInteger 1 + , coinCfg'aToken = toAToken name + , coinCfg'interestModel = Types.defaultInterestModel + , coinCfg'liquidationBonus = R.reduce 5 100 + } + ) + coinNames + + users = zipWith (\coinName userName -> (Types.UserId (PaymentPubKeyHash userName), wal (toCoin coinName, 100))) coinNames userNames + wal cs = BchWallet $ Hask.uncurry M.singleton cs + + toAToken name = Value.TokenName $ "a" <> name + +toCoin :: BuiltinByteString -> Types.Coin +toCoin str = Value.AssetClass (Value.CurrencySymbol str, Value.TokenName str) + +---------------------------------------------------------- +-- scripts + +type Script = Script.Script Types.Act + +-- | Make user act +userAct :: Types.UserId -> Types.UserAct -> Script +userAct uid act = do + time <- Script.getCurrentTime + Script.putAct $ Types.UserAct time uid act + +-- | Make price act +priceAct :: Types.UserId -> Types.PriceAct -> Script +priceAct uid arg = do + t <- Script.getCurrentTime + Script.putAct $ Types.PriceAct t uid arg + +-- | Make govern act +governAct :: Types.UserId -> Types.GovernAct -> Script +governAct uid arg = Script.putAct $ Types.GovernAct uid arg + +-- | Make query act +queryAct :: Types.UserId -> Types.QueryAct -> Script +queryAct uid arg = do + t <- Script.getCurrentTime + Script.putAct $ Types.QueryAct uid t arg diff --git a/mlabs/src/Mlabs/Lending/Logic/InterestRate.hs b/mlabs/src/Mlabs/Lending/Logic/InterestRate.hs new file mode 100644 index 000000000..51c8bbee4 --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Logic/InterestRate.hs @@ -0,0 +1,102 @@ +{-# LANGUAGE NamedFieldPuns #-} + +-- | Calculate interest rate parameters +module Mlabs.Lending.Logic.InterestRate ( + updateReserveInterestRates, + getLiquidityRate, + getNormalisedIncome, + getCumulatedLiquidityIndex, + addDeposit, + getCumulativeBalance, +) where + +import PlutusTx.Prelude + +-- import Prelude qualified as Hask (String) + +import Mlabs.Lending.Logic.Types (Reserve (..), ReserveInterest (..), Wallet (..)) +import Mlabs.Lending.Logic.Types qualified as Types +import PlutusTx.Ratio qualified as R + +{-# INLINEABLE updateReserveInterestRates #-} +updateReserveInterestRates :: Integer -> Types.Reserve -> Types.Reserve +updateReserveInterestRates currentTime reserve = + reserve {reserve'interest = nextInterest reserve} + where + nextInterest Types.Reserve {reserve'interest} = + reserve'interest + { ri'liquidityRate = liquidityRate + , ri'liquidityIndex = newIndex + , ri'normalisedIncome = newIncome + , ri'lastUpdateTime = currentTime + } + where + newIndex = + getCumulatedLiquidityIndex + liquidityRate + yearDelta + (ri'liquidityIndex reserve'interest) + newIncome = + getNormalisedIncome + liquidityRate + yearDelta + (ri'liquidityIndex reserve'interest) + yearDelta = getYearDelta lastUpdateTime currentTime + liquidityRate = getLiquidityRate reserve + lastUpdateTime = ri'lastUpdateTime reserve'interest + +{-# INLINEABLE getYearDelta #-} +getYearDelta :: Integer -> Integer -> Rational +getYearDelta t0 t1 = R.fromInteger (max 0 $ t1 - t0) * secondsPerSlot * R.recip secondsPerYear + where + secondsPerSlot = R.fromInteger 1 + secondsPerYear = R.fromInteger 31622400 + +{-# INLINEABLE getCumulatedLiquidityIndex #-} +getCumulatedLiquidityIndex :: Rational -> Rational -> Rational -> Rational +getCumulatedLiquidityIndex liquidityRate yearDelta prevLiquidityIndex = + (liquidityRate * yearDelta + R.fromInteger 1) * prevLiquidityIndex + +{-# INLINEABLE getNormalisedIncome #-} +getNormalisedIncome :: Rational -> Rational -> Rational -> Rational +getNormalisedIncome liquidityRate yearDelta prevLiquidityIndex = + (liquidityRate * yearDelta + R.fromInteger 1) * prevLiquidityIndex + +{-# INLINEABLE getLiquidityRate #-} +getLiquidityRate :: Types.Reserve -> Rational +getLiquidityRate Types.Reserve {..} = r * u + where + u = getUtilisation reserve'wallet + r = getBorrowRate (ri'interestModel reserve'interest) u + +{-# INLINEABLE getUtilisation #-} +getUtilisation :: Types.Wallet -> Rational +getUtilisation Types.Wallet {..} = R.reduce wallet'borrow liquidity + where + liquidity = wallet'deposit + wallet'borrow + +{-# INLINEABLE getBorrowRate #-} +getBorrowRate :: Types.InterestModel -> Rational -> Rational +getBorrowRate Types.InterestModel {..} u + | u <= uOptimal = im'base + im'slope1 * (u * R.recip uOptimal) + | otherwise = im'base + im'slope2 * (u - uOptimal) * R.recip (R.fromInteger 1 - uOptimal) + where + uOptimal = im'optimalUtilisation + +{-# INLINEABLE addDeposit #-} +addDeposit :: Rational -> Integer -> Types.Wallet -> Either BuiltinByteString Types.Wallet +addDeposit normalisedIncome amount wallet + | newDeposit >= 0 = + Right + wallet + { wallet'deposit = max 0 newDeposit + , wallet'scaledBalance = max (R.fromInteger 0) $ wallet'scaledBalance wallet + R.fromInteger amount * R.recip normalisedIncome + } + | otherwise = Left "Negative deposit" + where + newDeposit = wallet'deposit wallet + amount + +{-# INLINEABLE getCumulativeBalance #-} +getCumulativeBalance :: Rational -> Types.Wallet -> Rational +getCumulativeBalance normalisedIncome Types.Wallet {..} = + wallet'scaledBalance * normalisedIncome diff --git a/mlabs/src/Mlabs/Lending/Logic/React.hs b/mlabs/src/Mlabs/Lending/Logic/React.hs new file mode 100644 index 000000000..4e5c013bd --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Logic/React.hs @@ -0,0 +1,474 @@ +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-specialize #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fobject-code #-} + +-- | State transitions for Aave-like application +module Mlabs.Lending.Logic.React ( + react, + qReact, +) where + +import PlutusTx.Prelude + +import Control.Monad.Except (MonadError (throwError)) +import Control.Monad.State.Strict (MonadState (get, put), gets) +import Data.Semigroup (Last (..)) +import PlutusTx.AssocMap qualified as M +import PlutusTx.These (these) +import Prelude qualified as Hask + +import Mlabs.Control.Check (isNonNegative, isPositive, isPositiveRational, isUnitRange) +import Mlabs.Data.List qualified as L +import Mlabs.Emulator.Blockchain (Resp (Burn, Mint), moveFromTo) +import Mlabs.Lending.Logic.InterestRate (addDeposit) +import Mlabs.Lending.Logic.State qualified as State +import Mlabs.Lending.Logic.Types ( + BadBorrow (BadBorrow, badBorrow'asset, badBorrow'userId), + CoinCfg (coinCfg'aToken, coinCfg'coin, coinCfg'interestModel, coinCfg'liquidationBonus, coinCfg'rate), + CoinRate (CoinRate, coinRate'lastUpdateTime), + InterestModel (im'optimalUtilisation, im'slope1, im'slope2), + LendingPool (lp'coinMap, lp'healthReport, lp'reserves, lp'users), + Reserve (reserve'rate, reserve'wallet), + User (user'health, user'lastUpdateTime, user'wallets), + UserAct (..), + UserId (Self), + Wallet (wallet'borrow, wallet'collateral, wallet'deposit), + adaCoin, + initReserve, + ) +import Mlabs.Lending.Logic.Types qualified as Types +import PlutusTx.Ratio qualified as R + +-- import qualified Control.Monad.RWS.Strict as State +-- import PlutusCore.Name (isEmpty) + +{-# INLINEABLE qReact #-} + +-- | React to query actions by using the State Machine functions. +qReact :: Types.Act -> State.St (Maybe (Last Types.QueryRes)) +qReact input = do + case input of + Types.QueryAct uid t act -> queryAct uid t act + _ -> pure Nothing -- does nothing for any other type of input + where + queryAct uid time = \case + Types.QueryCurrentBalanceAct () -> queryCurrentBalance uid time + Types.QueryInsolventAccountsAct () -> queryInsolventAccounts uid time + + --------------------------------------------------------------------------------------------------------- + -- Current Balance Query + queryCurrentBalance :: Types.UserId -> Integer -> State.St (Maybe (Last Types.QueryRes)) + queryCurrentBalance uid _cTime = do + user <- State.getUser uid + tWallet <- State.getAllWallets uid + tDeposit <- State.getTotalDeposit user + tCollateral <- State.getTotalCollateral user + tBorrow <- State.getTotalBorrow user + tWalletCumulativeBalance <- State.getWalletCumulativeBalance uid + pure . Just . Last . Types.QueryResCurrentBalance $ + Types.UserBalance + { ub'id = uid + , ub'totalDeposit = tDeposit + , ub'totalCollateral = tCollateral + , ub'totalBorrow = tBorrow + , ub'cumulativeBalance = tWalletCumulativeBalance + , ub'funds = tWallet + } + + --------------------------------------------------------------------------------------------------------- + -- Insolvent Accounts Query + -- Returns a list of users where the health of a coin is under 1, together + -- with the health of the coin. Only admins can use. + queryInsolventAccounts :: Types.UserId -> Integer -> State.St (Maybe (Last Types.QueryRes)) + queryInsolventAccounts uid _cTime = do + State.isAdmin uid -- check user is admin + allUsersIds :: [UserId] <- M.keys <$> State.getAllUsers + allUsers :: [User] <- M.elems <$> State.getAllUsers + userWCoins :: [(UserId, (User, [Types.Coin]))] <- + fmap (zip allUsersIds . zip allUsers) $ + sequence $ flip State.getsAllWallets M.keys <$> allUsersIds + insolventUsers :: [(UserId, [(Types.Coin, Rational)])] <- sequence $ fmap aux userWCoins + let onlyInsolventUsers = filter (not . null . snd) insolventUsers -- Remove the users with no insolvent coins. + pure . wrap $ uncurry Types.InsolventAccount <$> onlyInsolventUsers + where + aux :: (UserId, (User, [Types.Coin])) -> State.St (UserId, [(Types.Coin, Rational)]) + aux = \(uId, (user, coins)) -> do + y <- sequence $ flip State.getCurrentHealthCheck user <$> coins + let coins' = fst <$> filter snd (zip coins y) + y' <- sequence $ flip State.getCurrentHealth user <$> coins' + let coins'' = zip coins' y' + pure $ (,) uId coins'' + + wrap = Just . Last . Types.QueryResInsolventAccounts + +{-# INLINEABLE react #-} + +{- | State transitions for lending pool. + For a given action we update internal state of Lending pool and produce + list of responses to simulate change of the balances on blockchain. +-} +react :: Types.Act -> State.St [Resp] +react input = do + checkInput input + case input of + Types.UserAct t uid act -> withHealthCheck t $ userAct t uid act + Types.PriceAct t uid act -> withHealthCheck t $ priceAct t uid act + Types.GovernAct uid act -> governAct uid act + Types.QueryAct {} -> pure [] -- A query should produce no state modifying actions + where + userAct time uid = \case + Types.DepositAct {..} -> depositAct time uid act'amount act'asset + Types.BorrowAct {..} -> borrowAct time uid act'asset act'amount act'rate + Types.RepayAct {..} -> repayAct time uid act'asset act'amount act'rate + Types.SwapBorrowRateModelAct {..} -> swapBorrowRateModelAct uid act'asset act'rate + Types.AddCollateralAct {..} -> addCollateral uid add'asset add'amount + Types.RemoveCollateralAct {..} -> removeCollateral uid remove'asset remove'amount + Types.WithdrawAct {..} -> withdrawAct time uid act'amount act'asset + Types.FlashLoanAct -> flashLoanAct uid + Types.LiquidationCallAct {..} -> liquidationCallAct time uid act'collateral act'debt act'debtToCover act'receiveAToken + + --------------------------------------------------- + -- deposit + + depositAct currentTime uid amount asset = do + ni <- State.getNormalisedIncome asset + State.modifyWalletAndReserve' uid asset (addDeposit ni amount) + aCoin <- State.aToken asset + State.updateReserveState currentTime asset + pure $ + mconcat + [ [Mint aCoin amount] + , moveFromTo Self uid aCoin amount + , moveFromTo uid Self asset amount + ] + + --------------------------------------------------- + -- borrow + + -- TODO: ignores rate strategy (stable vs variable), ratio of liquidity to borrowed totals, health-check + -- For borrowing to be valid we check that + -- * reserve has enough liquidity + -- * user does not use collateral reserve to borrow (it's meaningless for the user) + -- * user has enough collateral for the borrow + borrowAct currentTime uid asset amount _rate = do + hasEnoughLiquidityToBorrow asset amount + collateralNonBorrow uid asset + hasEnoughCollateral uid asset amount + updateOnBorrow + State.updateReserveState currentTime asset + pure $ moveFromTo Self uid asset amount + where + updateOnBorrow = do + ni <- State.getNormalisedIncome asset + State.modifyWallet uid asset $ \w -> w {wallet'borrow = wallet'borrow w + amount} + State.modifyReserveWallet' asset $ addDeposit ni (negate amount) + + hasEnoughLiquidityToBorrow asset amount = do + liquidity <- State.getsReserve asset (wallet'deposit . reserve'wallet) + State.guardError "Not enough liquidity for asset" (liquidity >= amount) + + collateralNonBorrow uid asset = do + col <- State.getsWallet uid asset wallet'collateral + State.guardError + "Collateral can not be used as borrow for user" + (col == 0) + + hasEnoughCollateral uid asset amount = do + bor <- State.toAda asset amount + isOk <- State.getHealthCheck bor asset =<< State.getUser uid + State.guardError msg isOk + where + msg = "Not enough collateral to borrow" + + --------------------------------------------------- + -- repay (also called redeem in whitepaper) + + repayAct currentTime uid asset amount _rate = do + ni <- State.getNormalisedIncome asset + bor <- State.getsWallet uid asset wallet'borrow + let newBor = bor - amount + if newBor >= 0 + then State.modifyWallet uid asset $ \w -> w {wallet'borrow = newBor} + else State.modifyWallet' uid asset $ \w -> do + w1 <- addDeposit ni (negate newBor) w + pure $ w1 {wallet'borrow = 0} + State.modifyReserveWallet' asset $ addDeposit ni amount + State.updateReserveState currentTime asset + pure $ moveFromTo uid Self asset amount + + --------------------------------------------------- + -- swap borrow model + + swapBorrowRateModelAct _ _ _ = todo + + --------------------------------------------------- + -- todo docs + -- set user reserve as collateral + + addCollateral uid asset desiredAmount + | desiredAmount <= 0 = + pure [] + | otherwise = + do + ni <- State.getNormalisedIncome asset + amount <- calcAmountFor wallet'deposit uid asset desiredAmount + State.modifyWallet' uid asset $ \w -> do + w1 <- addDeposit ni (negate amount) w + pure $ w1 {wallet'collateral = wallet'collateral w + amount} + aCoin <- State.aToken asset + pure $ moveFromTo uid Self aCoin amount + + removeCollateral uid asset desiredAmount + | desiredAmount <= 0 = + pure [] + | otherwise = + do + ni <- State.getNormalisedIncome asset + amount <- calcAmountFor wallet'collateral uid asset desiredAmount + State.modifyWalletAndReserve' uid asset $ \w -> do + w1 <- addDeposit ni amount w + pure $ w1 {wallet'collateral = wallet'collateral w - amount} + aCoin <- State.aToken asset + pure $ moveFromTo Self uid aCoin amount + + calcAmountFor extract uid asset desiredAmount = do + availableAmount <- State.getsWallet uid asset extract + pure $ min availableAmount desiredAmount + + --------------------------------------------------- + -- withdraw + + withdrawAct currentTime uid amount asset = do + -- validate withdraw + hasEnoughDepositToWithdraw uid amount asset + -- update state on withdraw + ni <- State.getNormalisedIncome asset + State.modifyWalletAndReserve' uid asset $ addDeposit ni (negate amount) + aCoin <- State.aToken asset + State.updateReserveState currentTime asset + pure $ + mconcat + [ moveFromTo Self uid asset amount + , moveFromTo uid Self aCoin amount + , Hask.pure $ Burn aCoin amount + ] + + hasEnoughDepositToWithdraw uid amount asset = do + dep <- State.getCumulativeBalance uid asset + State.guardError "Not enough deposit to withdraw" (dep >= R.fromInteger amount) + + --------------------------------------------------- + -- flash loan + + flashLoanAct _ = todo + + --------------------------------------------------- + -- liquidation call + + liquidationCallAct currentTime uid collateralAsset debt amountCovered receiveATokens = do + isBadBorrow debt + wals <- State.getsUser (badBorrow'userId debt) user'wallets + bor <- getDebtValue wals + col <- getCollateralValue wals + isPositive "liquidation collateral" col + debtAmountIsLessThanHalf bor amountCovered + colCovered <- min col <$> getCollateralCovered amountCovered + adaBonus <- getBonus colCovered + aCollateralAsset <- State.aToken collateralAsset + updateBorrowUser colCovered + pure $ + mconcat + [ moveFromTo uid Self borrowAsset amountCovered + , moveFromTo Self uid (receiveAsset aCollateralAsset) colCovered + , moveFromTo Self uid adaCoin adaBonus + ] + where + borrowAsset = badBorrow'asset debt + borrowUserId = badBorrow'userId debt + + receiveAsset aCoin + | receiveATokens = aCoin + | otherwise = collateralAsset + + getDebtValue wals = case M.lookup borrowAsset wals of + Just wal -> pure $ wallet'borrow wal + Nothing -> throwError "Wallet does not have the debt to liquidate" + + getCollateralValue wals = case M.lookup collateralAsset wals of + Just wal -> pure $ wallet'collateral wal + Nothing -> throwError "Wallet does not have collateral for liquidation asset" + + debtToColateral = + State.convertCoin + State.Convert + { convert'from = borrowAsset + , convert'to = collateralAsset + } + + getCollateralCovered amount = debtToColateral amount + + getBonus amount = do + rate <- State.getLiquidationBonus collateralAsset + State.toAda collateralAsset $ R.round $ R.fromInteger amount * rate + + debtAmountIsLessThanHalf userDebt amount + | userDebt >= 2 * amount = pure () + | otherwise = throwError "Can not cover more than half of the borrow" + + -- we remove part of the borrow from the user and part of the collateral + updateBorrowUser colCovered = do + State.modifyWalletAndReserve borrowUserId collateralAsset $ \w -> + w {wallet'collateral = wallet'collateral w - colCovered} + State.modifyWalletAndReserve borrowUserId borrowAsset $ \w -> + w {wallet'borrow = wallet'borrow w - amountCovered} + updateSingleUserHealth currentTime borrowUserId + + isBadBorrow bor = do + isOk <- M.member bor <$> gets lp'healthReport + State.guardError "Bad borrow not present" isOk + + --------------------------------------------------- + priceAct currentTime uid act = do + State.isTrustedOracle uid + case act of + Types.SetAssetPriceAct coin rate -> setAssetPrice currentTime coin rate + + --------------------------------------------------- + -- update on market price change + + setAssetPrice currentTime asset rate = do + State.modifyReserve asset $ \r -> r {reserve'rate = CoinRate rate currentTime} + pure [] + + --------------------------------------------------- + -- Govern acts + + governAct uid act = do + State.isAdmin uid + case act of + Types.AddReserveAct cfg -> addReserve cfg + + --------------------------------------------------- + -- Adds new reserve (new coin/asset) + + addReserve cfg@Types.CoinCfg {..} = do + st <- get + State.guardError "Reserve is already present" $ + M.member coinCfg'coin (lp'reserves st) + let newReserves = M.insert coinCfg'coin (initReserve cfg) $ lp'reserves st + newCoinMap = M.insert coinCfg'aToken coinCfg'coin $ lp'coinMap st + put $ st {lp'reserves = newReserves, lp'coinMap = newCoinMap} + return [] + + --------------------------------------------------- + -- health checks + + withHealthCheck time act = do + res <- act + updateHealthChecks time + return res + + updateHealthChecks currentTime = do + us <- getUsersForUpdate + newUsers <- M.fromList <$> mapM (updateUserHealth currentTime) us + State.modifyUsers $ \users -> batchInsert newUsers users + where + getUsersForUpdate = do + us <- fmap setTimestamp . M.toList <$> gets lp'users + pure $ fmap snd $ L.take userUpdateSpan $ L.sortOn fst us + + setTimestamp (uid, user) = (user'lastUpdateTime user - currentTime, (uid, user)) + + updateSingleUserHealth currentTime uid = do + user <- State.getUser uid + newUser <- snd <$> updateUserHealth currentTime (uid, user) + State.modifyUser uid $ const newUser + + updateUserHealth currentTime (uid, user) = do + health <- mapM (\asset -> (asset,) <$> State.getHealth 0 asset user) userBorrows + L.mapM_ (reportUserHealth uid) health + pure + ( uid + , user + { user'lastUpdateTime = currentTime + , user'health = M.fromList health + } + ) + where + userBorrows = M.keys $ M.filter ((> 0) . wallet'borrow) $ user'wallets user + + reportUserHealth uid (asset, health) + | health >= R.fromInteger 1 = State.modifyHealthReport $ M.delete (BadBorrow uid asset) + | otherwise = State.modifyHealthReport $ M.insert (BadBorrow uid asset) health + + -- insert m1 to m2 + batchInsert m1 m2 = fmap (these id id const) $ M.union m1 m2 + + -- how many users to update per iteration of update health checks + userUpdateSpan = 10 + + todo = return [] + +{-# INLINEABLE checkInput #-} + +-- | Check if input is valid +checkInput :: Types.Act -> State.St () +checkInput = \case + Types.UserAct time _uid act -> do + isNonNegative "timestamp" time + checkUserAct act + Types.PriceAct time _uid act -> checkPriceAct time act + Types.GovernAct _uid act -> checkGovernAct act + Types.QueryAct {} -> pure () -- TODO think of input checks for query + where + checkUserAct = \case + Types.DepositAct amount asset -> do + isPositive "deposit" amount + State.isAsset asset + Types.BorrowAct amount asset _rate -> do + isPositive "borrow" amount + State.isAsset asset + Types.RepayAct amount asset _rate -> do + isPositive "repay" amount + State.isAsset asset + Types.SwapBorrowRateModelAct asset _rate -> State.isAsset asset + Types.AddCollateralAct asset amount -> do + State.isAsset asset + isPositive "add collateral" amount + Types.RemoveCollateralAct asset amount -> do + State.isAsset asset + isPositive "remove collateral" amount + Types.WithdrawAct asset amount -> do + isPositive "withdraw" amount + State.isAsset asset + Types.FlashLoanAct -> pure () + Types.LiquidationCallAct collateral _debt debtToCover _recieveAToken -> do + State.isAsset collateral + isPositive "Debt to cover" debtToCover + + checkPriceAct time act = do + isNonNegative "price rate timestamp" time + case act of + Types.SetAssetPriceAct asset price -> do + checkCoinRateTimeProgress time asset + isPositiveRational "price" price + State.isAsset asset + + checkGovernAct = \case + Types.AddReserveAct cfg -> checkCoinCfg cfg + + checkCoinCfg Types.CoinCfg {..} = do + isPositiveRational "coin price config" coinCfg'rate + checkInterestModel coinCfg'interestModel + isUnitRange "liquidation bonus config" coinCfg'liquidationBonus + + checkInterestModel Types.InterestModel {..} = do + isUnitRange "optimal utilisation" im'optimalUtilisation + isPositiveRational "slope 1" im'slope1 + isPositiveRational "slope 2" im'slope2 + + checkCoinRateTimeProgress time asset = do + lastUpdateTime <- coinRate'lastUpdateTime . reserve'rate <$> State.getReserve asset + isNonNegative "Timestamps for price update should grow" (time - lastUpdateTime) diff --git a/mlabs/src/Mlabs/Lending/Logic/State.hs b/mlabs/src/Mlabs/Lending/Logic/State.hs new file mode 100644 index 000000000..c5f40b6ee --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Logic/State.hs @@ -0,0 +1,448 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-specialize #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fobject-code #-} + +-- | State transitions for Lending app +module Mlabs.Lending.Logic.State ( + St, + Error, + isAsset, + aToken, + isAdmin, + isTrustedOracle, + updateReserveState, + initReserve, + guardError, + getWallet, + getAllWallets, + getsWallet, + getsAllWallets, + getUser, + getsUser, + getAllUsers, + getsAllUsers, + getUsers, + getReserve, + getsReserve, + toAda, + fromAda, + Convert (..), + reverseConvert, + convertCoin, + getTotalCollateral, + getTotalBorrow, + getTotalDeposit, + getLiquidationThreshold, + getLiquidationBonus, + getHealth, + getCurrentHealthCheck, + getHealthCheck, + getCurrentHealth, + modifyUsers, + modifyReserve, + modifyReserveWallet, + modifyUser, + modifyWallet, + modifyWalletAndReserve, + modifyReserve', + modifyReserveWallet', + modifyUser', + modifyWallet', + modifyWalletAndReserve', + modifyHealthReport, + getNormalisedIncome, + getCumulativeBalance, + getWalletCumulativeBalance, +) where + +import PlutusTx.Prelude +import Prelude qualified as Hask (Show, uncurry) + +import Control.Monad.Except (MonadError (throwError)) +import Control.Monad.State.Strict (MonadState (get, put), gets, modify') +import PlutusTx.AssocMap (Map) +import PlutusTx.AssocMap qualified as M +import PlutusTx.Numeric qualified as N + +import Mlabs.Control.Monad.State (PlutusState, guardError) +import Mlabs.Lending.Logic.InterestRate qualified as IR +import Mlabs.Lending.Logic.Types ( + CoinRate (coinRate'value), + LendingPool ( + lp'admins, + lp'healthReport, + lp'reserves, + lp'trustedOracles, + lp'users + ), + Reserve ( + reserve'interest, + reserve'liquidationBonus, + reserve'liquidationThreshold, + reserve'rate, + reserve'wallet + ), + ReserveInterest (ri'normalisedIncome), + User (User, user'wallets), + Wallet (wallet'borrow, wallet'collateral, wallet'deposit), + defaultUser, + defaultWallet, + initReserve, + toLendingToken, + ) +import Mlabs.Lending.Logic.Types qualified as Types +import PlutusTx.Ratio qualified as R + +-- | Type for errors +type Error = BuiltinByteString + +-- | State update of lending pool +type St = PlutusState LendingPool + +---------------------------------------------------- +-- common functions + +{-# INLINEABLE isAsset #-} + +-- | Check that lending pool supports given asset +isAsset :: Types.Coin -> St () +isAsset asset = do + reserves <- gets lp'reserves + if M.member asset reserves + then pure () + else throwError "Asset not supported" + +{-# INLINEABLE updateReserveState #-} + +{- | Updates all iterative parameters of reserve. + Reserve state controls interest rates and health checks for all users. +-} +updateReserveState :: Integer -> Types.Coin -> St () +updateReserveState currentTime asset = + modifyReserve asset $ IR.updateReserveInterestRates currentTime + +{-# INLINEABLE isTrustedOracle #-} + +-- | check that user is allowed to do oracle actions +isTrustedOracle :: Types.UserId -> St () +isTrustedOracle = checkRole "Is not trusted oracle" lp'trustedOracles + +{-# INLINEABLE isAdmin #-} + +-- | check that user is allowed to do admin actions +isAdmin :: Types.UserId -> St () +isAdmin = checkRole "Is not admin" lp'admins + +{-# INLINEABLE checkRole #-} +checkRole :: BuiltinByteString -> (LendingPool -> [Types.UserId]) -> Types.UserId -> St () +checkRole msg extract uid = do + users <- gets extract + guardError msg $ elem uid users + +{-# INLINEABLE aToken #-} +aToken :: Types.Coin -> St Types.Coin +aToken coin = do + mCoin <- gets (`toLendingToken` coin) + maybe err pure mCoin + where + err = throwError "Coin not supported" + +{-# INLINEABLE getsWallet #-} + +{- | Read field from the internal wallet for user and on asset. + If there is no wallet empty wallet is allocated. +-} +getsWallet :: Types.UserId -> Types.Coin -> (Wallet -> a) -> St a +getsWallet uid coin f = fmap f $ getWallet uid coin + +-- | Get internal wallet for user on given asset. +{-# INLINEABLE getWallet #-} +getWallet :: Types.UserId -> Types.Coin -> St Wallet +getWallet uid coin = + getsUser uid (fromMaybe defaultWallet . M.lookup coin . user'wallets) + +-- | Get all user internal wallets. +{-# INLINEABLE getsAllWallets #-} +getsAllWallets :: Types.UserId -> (Map Types.Coin Wallet -> a) -> St a +getsAllWallets uid f = + f <$> getAllWallets uid + +-- | Get all user internal wallets. +{-# INLINEABLE getAllWallets #-} +getAllWallets :: Types.UserId -> St (Map Types.Coin Wallet) +getAllWallets uid = + getsUser uid user'wallets + +{-# INLINEABLE getUsers #-} + +-- | Get a list of all the users. +getUsers :: St [Types.UserId] +getUsers = M.keys <$> getAllUsers + +{-# INLINEABLE getsUser #-} + +-- | Get user info in the lending app by user id and apply extractor function to it. +getsUser :: Types.UserId -> (User -> a) -> St a +getsUser uid f = fmap f $ getUser uid + +{-# INLINEABLE getUser #-} + +-- | Get user info in the lending app by user id. +getUser :: Types.UserId -> St User +getUser uid = gets (fromMaybe defaultUser . M.lookup uid . lp'users) + +{-# INLINEABLE getAllUsers #-} + +-- | Get Map of all users. +getAllUsers :: St (Map Types.UserId User) +getAllUsers = gets lp'users + +{-# INLINEABLE getsAllUsers #-} + +-- | Gets all users given predicate. +getsAllUsers :: (User -> Bool) -> St (Map Types.UserId User) +getsAllUsers f = gets (M.filter f . lp'users) + +{-# INLINEABLE getsReserve #-} + +-- | Read reserve for a given asset and apply extractor function to it. +getsReserve :: Types.Coin -> (Reserve -> a) -> St a +getsReserve coin extract = fmap extract $ getReserve coin + +{-# INLINEABLE getReserve #-} + +-- | Read reserve for a given asset. +getReserve :: Types.Coin -> St Reserve +getReserve coin = do + mReserve <- gets (M.lookup coin . lp'reserves) + maybe err pure mReserve + where + err = throwError "Uknown coin" + +{-# INLINEABLE toAda #-} + +-- | Convert given currency to base currency +toAda :: Types.Coin -> Integer -> St Integer +toAda coin val = do + ratio' <- fmap (coinRate'value . reserve'rate) $ getReserve coin + pure $ R.round $ R.fromInteger val N.* ratio' + +{-# INLINEABLE fromAda #-} + +-- | Convert given currency from base currency +fromAda :: Types.Coin -> Integer -> St Integer +fromAda coin val = do + ratio' <- fmap (coinRate'value . reserve'rate) $ getReserve coin + pure $ R.round $ R.fromInteger val N.* R.recip ratio' + +-- | Conversion between coins +data Convert = Convert + { -- | convert from + convert'from :: Types.Coin + , -- | convert to + convert'to :: Types.Coin + } + deriving stock (Hask.Show) + +{-# INLINEABLE reverseConvert #-} +reverseConvert :: Convert -> Convert +reverseConvert Convert {..} = + Convert + { convert'from = convert'to + , convert'to = convert'from + } + +{-# INLINEABLE convertCoin #-} + +-- | Converts from one currency to another +convertCoin :: Convert -> Integer -> St Integer +convertCoin Convert {..} amount = + fromAda convert'to =<< toAda convert'from amount + +{-# INLINEABLE weightedTotal #-} + +-- | Weigted total of currencies in base currency +weightedTotal :: [(Types.Coin, Integer)] -> St Integer +weightedTotal = fmap sum . mapM (Hask.uncurry toAda) + +{-# INLINEABLE walletTotal #-} + +-- | Collects cumulative value for given wallet field +walletTotal :: (Wallet -> Integer) -> User -> St Integer +walletTotal extract (User ws _ _) = weightedTotal $ M.toList $ fmap extract ws + +{-# INLINEABLE getTotalCollateral #-} + +-- | Gets total collateral for a user. +getTotalCollateral :: User -> St Integer +getTotalCollateral = walletTotal wallet'collateral + +{-# INLINEABLE getTotalBorrow #-} + +-- | Gets total borrows for a user in base currency. +getTotalBorrow :: User -> St Integer +getTotalBorrow = walletTotal wallet'borrow + +{-# INLINEABLE getTotalDeposit #-} + +-- | Gets total deposit for a user in base currency. +getTotalDeposit :: User -> St Integer +getTotalDeposit = walletTotal wallet'deposit + +{-# INLINEABLE getHealthCheck #-} + +-- | Check if the user has enough health for the given asset. +getHealthCheck :: Integer -> Types.Coin -> User -> St Bool +getHealthCheck addToBorrow coin user = + fmap (> R.fromInteger 1) $ getHealth addToBorrow coin user + +{-# INLINEABLE getCurrentHealthCheck #-} + +-- | Check if the user has currently enough health for the given asset. +getCurrentHealthCheck :: Types.Coin -> User -> St Bool +getCurrentHealthCheck = getHealthCheck 0 + +{-# INLINEABLE getHealth #-} + +-- | Check borrowing health for the user by given currency +getHealth :: Integer -> Types.Coin -> User -> St Rational +getHealth addToBorrow coin user = do + col <- getTotalCollateral user + bor <- fmap (+ addToBorrow) $ getTotalBorrow user + liq <- getLiquidationThreshold coin + pure $ R.fromInteger col N.* liq N.* R.recip (R.fromInteger bor) + +{-# INLINEABLE getCurrentHealth #-} + +-- | Check immediate borrowing health for the user by given currency +getCurrentHealth :: Types.Coin -> User -> St Rational +getCurrentHealth = getHealth 0 + +{-# INLINEABLE getLiquidationThreshold #-} + +-- | Reads liquidation threshold for a give asset. +getLiquidationThreshold :: Types.Coin -> St Rational +getLiquidationThreshold coin = + gets (maybe (R.fromInteger 0) reserve'liquidationThreshold . M.lookup coin . lp'reserves) + +{-# INLINEABLE getLiquidationBonus #-} + +-- | Reads liquidation bonus for a give asset. +getLiquidationBonus :: Types.Coin -> St Rational +getLiquidationBonus coin = + gets (maybe (R.fromInteger 0) reserve'liquidationBonus . M.lookup coin . lp'reserves) + +{-# INLINEABLE modifyUsers #-} +modifyUsers :: (Map Types.UserId User -> Map Types.UserId User) -> St () +modifyUsers f = modify' $ \lp -> lp {lp'users = f $ lp'users lp} + +{-# INLINEABLE modifyReserve #-} + +-- | Modify reserve for a given asset. +modifyReserve :: Types.Coin -> (Reserve -> Reserve) -> St () +modifyReserve coin f = modifyReserve' coin (Right . f) + +{-# INLINEABLE modifyReserve' #-} + +-- | Modify reserve for a given asset. It can throw errors. +modifyReserve' :: Types.Coin -> (Reserve -> Either Error Reserve) -> St () +modifyReserve' asset f = do + st <- get + case M.lookup asset $ lp'reserves st of + Just reserve -> either throwError (putReserve st) (f reserve) + Nothing -> throwError "Asset is not supported" + where + putReserve st x = put $ st {lp'reserves = M.insert asset x $ lp'reserves st} + +{-# INLINEABLE modifyUser #-} + +-- | Modify user info by id. +modifyUser :: Types.UserId -> (User -> User) -> St () +modifyUser uid f = modifyUser' uid (Right . f) + +{-# INLINEABLE modifyUser' #-} + +-- | Modify user info by id. It can throw errors. +modifyUser' :: Types.UserId -> (User -> Either Error User) -> St () +modifyUser' uid f = do + st <- get + let poolUsers = lp'users st + case f $ fromMaybe defaultUser $ M.lookup uid poolUsers of + Left msg -> throwError msg + Right user -> put $ st {lp'users = M.insert uid user poolUsers} + +{-# INLINEABLE modifyHealthReport #-} +modifyHealthReport :: (Types.HealthReport -> Types.HealthReport) -> St () +modifyHealthReport f = + modify' $ \lp -> lp {lp'healthReport = f $ lp'healthReport lp} + +{-# INLINEABLE modifyWalletAndReserve #-} + +-- | Modify user wallet and reserve wallet with the same function. +modifyWalletAndReserve :: Types.UserId -> Types.Coin -> (Wallet -> Wallet) -> St () +modifyWalletAndReserve uid coin f = modifyWalletAndReserve' uid coin (Right . f) + +{-# INLINEABLE modifyWalletAndReserve' #-} + +-- | Applies the same modification function to the user and to the reserve wallet. It can throw errors. +modifyWalletAndReserve' :: Types.UserId -> Types.Coin -> (Wallet -> Either Error Wallet) -> St () +modifyWalletAndReserve' uid coin f = do + modifyWallet' uid coin f + modifyReserveWallet' coin f + +{-# INLINEABLE modifyReserveWallet #-} + +-- | Modify reserve wallet for a given asset. +modifyReserveWallet :: Types.Coin -> (Wallet -> Wallet) -> St () +modifyReserveWallet coin f = modifyReserveWallet' coin (Right . f) + +{-# INLINEABLE modifyReserveWallet' #-} + +-- | Modify reserve wallet for a given asset. It can throw errors. +modifyReserveWallet' :: Types.Coin -> (Wallet -> Either Error Wallet) -> St () +modifyReserveWallet' coin f = + modifyReserve' coin $ \r -> fmap (\w -> r {reserve'wallet = w}) $ f $ reserve'wallet r + +{-# INLINEABLE modifyWallet #-} + +-- | Modify internal user wallet that is allocated for a given user id and asset. +modifyWallet :: Types.UserId -> Types.Coin -> (Wallet -> Wallet) -> St () +modifyWallet uid coin f = modifyWallet' uid coin (Right . f) + +{-# INLINEABLE modifyWallet' #-} + +{- | Modify internal user wallet that is allocated for a given user id and asset. + It can throw errors. +-} +modifyWallet' :: Types.UserId -> Types.Coin -> (Wallet -> Either Error Wallet) -> St () +modifyWallet' uid coin f = modifyUser' uid $ \(User ws time health) -> do + wal <- f $ fromMaybe defaultWallet $ M.lookup coin ws + pure $ User (M.insert coin wal ws) time health + +{-# INLINEABLE getNormalisedIncome #-} +getNormalisedIncome :: Types.Coin -> St Rational +getNormalisedIncome asset = + getsReserve asset (ri'normalisedIncome . reserve'interest) + +{-# INLINEABLE getCumulativeBalance #-} +getCumulativeBalance :: Types.UserId -> Types.Coin -> St Rational +getCumulativeBalance uid asset = do + ni <- getNormalisedIncome asset + getsWallet uid asset (IR.getCumulativeBalance ni) + +{-# INLINEABLE getWalletCumulativeBalance #-} +getWalletCumulativeBalance :: Types.UserId -> St (Map Types.Coin Rational) +getWalletCumulativeBalance uid = do + wallet <- getsAllWallets uid M.toList :: St [(Types.Coin, Wallet)] + coins <- return $ fst <$> wallet :: St [Types.Coin] + ni <- mapM getNormalisedIncome coins + return . M.fromList $ zip coins ni diff --git a/mlabs/src/Mlabs/Lending/Logic/Types.hs b/mlabs/src/Mlabs/Lending/Logic/Types.hs new file mode 100644 index 000000000..4fa0672e3 --- /dev/null +++ b/mlabs/src/Mlabs/Lending/Logic/Types.hs @@ -0,0 +1,603 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-specialize #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fobject-code #-} + +{- | Types for lending app + + inspired by aave spec. See + + * https://docs.aave.com/developers/v/2.0/the-core-protocol/lendingpool +-} +module Mlabs.Lending.Logic.Types ( + LendingPool (..), + LendexId (..), + Wallet (..), + defaultWallet, + User (..), + defaultUser, + UserId (..), + Reserve (..), + ReserveInterest (..), + InterestRate (..), + InterestModel (..), + defaultInterestModel, + CoinCfg (..), + CoinRate (..), + adaCoin, + initReserve, + initLendingPool, + Act (..), + QueryAct (..), + UserAct (..), + StartParams (..), + HealthReport, + BadBorrow (..), + PriceAct (..), + GovernAct (..), + Coin, + toLendingToken, + fromLendingToken, + fromAToken, + QueryRes (..), + SupportedCurrency (..), + UserBalance (..), + InsolventAccount (..), +) where + +import PlutusTx.Prelude + +import Data.Aeson (FromJSON, ToJSON) +import GHC.Generics (Generic) +import Playground.Contract (ToSchema) +import Plutus.V1.Ledger.Crypto (PubKeyHash) +import Plutus.V1.Ledger.Tx (Address) +import Plutus.V1.Ledger.Value (AssetClass (..), CurrencySymbol (..), TokenName (..), Value) +import PlutusTx qualified +import PlutusTx.AssocMap (Map) +import PlutusTx.AssocMap qualified as M +import PlutusTx.Ratio qualified as R +import Prelude qualified as Hask (Eq, Show) + +import Mlabs.Emulator.Types (Coin, UserId (..), adaCoin) + +-- | Unique identifier of the lending pool state. +newtype LendexId = LendexId BuiltinByteString + deriving stock (Hask.Show, Generic) + deriving newtype (Eq) + deriving anyclass (ToJSON, FromJSON) + +-- | Lending pool is a list of reserves +data LendingPool = LendingPool + { -- | list of reserves + lp'reserves :: !(Map Coin Reserve) + , -- | internal user wallets on the app + lp'users :: !(Map UserId User) + , -- | main currencySymbol of the app + lp'currency :: !CurrencySymbol + , -- | maps aTokenNames to actual coins + lp'coinMap :: !(Map TokenName Coin) + , -- | map of unhealthy borrows + lp'healthReport :: !HealthReport + , -- | we accept govern acts only for those users + lp'admins :: ![UserId] + , -- | we accept price changes only for those users + lp'trustedOracles :: ![UserId] + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq LendingPool where + {-# INLINEABLE (==) #-} + (LendingPool r1 us1 c1 cm1 hr1 as1 tos1) == (LendingPool r2 us2 c2 cm2 hr2 as2 tos2) = + and + [ r1 == r2 + , us1 == us2 + , c1 == c2 + , cm1 == cm2 + , hr1 == hr2 + , as1 == as2 + , tos1 == tos2 + ] + +{- | Reserve of give coin in the pool. + It holds all info on individual collaterals and deposits. +-} +data Reserve = Reserve + { -- | total amounts of coins deposited to reserve + reserve'wallet :: !Wallet + , -- | ratio of reserve's coin to base currency + reserve'rate :: !CoinRate + , -- | ratio at which liquidation of collaterals can happen for this coin + reserve'liquidationThreshold :: !Rational + , -- | ratio of bonus for liquidation of the borrow in collateral of this asset + reserve'liquidationBonus :: !Rational + , -- | aToken corresponding to the coin of the reserve + reserve'aToken :: !TokenName + , -- | reserve liquidity params + reserve'interest :: !ReserveInterest + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq Reserve where + {-# INLINEABLE (==) #-} + (Reserve w1 r1 lt1 lb1 t1 i1) == (Reserve w2 r2 lt2 lb2 t2 i2) = + and + [ w1 == w2 + , r1 == r2 + , lt1 == lt2 + , lb1 == lb2 + , t1 == t2 + , i1 == i2 + ] + +data StartParams = StartParams + { -- | supported coins with ratios to ADA + sp'coins :: [CoinCfg] + , -- | init value deposited to the lending app + sp'initValue :: Value + , -- | admins + sp'admins :: [PubKeyHash] + , -- | trusted oracles + sp'oracles :: [PubKeyHash] + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +type HealthReport = Map BadBorrow Rational + +{- | Borrow that doesn't have enough collateral. + It has health check ration below one. +-} +data BadBorrow = BadBorrow + { -- | user identifier + badBorrow'userId :: !UserId + , -- | asset of the borrow + badBorrow'asset :: !Coin + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) + +instance Eq BadBorrow where + {-# INLINEABLE (==) #-} + BadBorrow a1 b1 == BadBorrow a2 b2 = a1 == a2 && b1 == b2 + +-- | Price of the given currency to Ada. +data CoinRate = CoinRate + { -- | ratio to ada + coinRate'value :: !Rational + , -- | last time price was updated + coinRate'lastUpdateTime :: !Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq CoinRate where + {-# INLINEABLE (==) #-} + CoinRate v1 lut1 == CoinRate v2 lut2 = + v1 == v2 && lut1 == lut2 + +-- | Parameters for calculation of interest rates. +data ReserveInterest = ReserveInterest + { ri'interestModel :: !InterestModel + , ri'liquidityRate :: !Rational + , ri'liquidityIndex :: !Rational + , ri'normalisedIncome :: !Rational + , ri'lastUpdateTime :: !Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq ReserveInterest where + {-# INLINEABLE (==) #-} + (ReserveInterest im1 lr1 li1 ni1 lut1) == (ReserveInterest im2 lr2 li2 ni2 lut2) = + and + [ im1 == im2 + , lr1 == lr2 + , li1 == li2 + , ni1 == ni2 + , lut1 == lut2 + ] + +data InterestModel = InterestModel + { im'optimalUtilisation :: !Rational + , im'slope1 :: !Rational + , im'slope2 :: !Rational + , im'base :: !Rational + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +instance Eq InterestModel where + {-# INLINEABLE (==) #-} + (InterestModel ou1 s11 s21 b1) == (InterestModel ou2 s12 s22 b2) = + and + [ ou1 == ou2 + , s11 == s12 + , s21 == s22 + , b1 == b2 + ] + +defaultInterestModel :: InterestModel +defaultInterestModel = + InterestModel + { im'base = R.fromInteger 0 + , im'slope1 = R.reduce 1 5 + , im'slope2 = R.fromInteger 4 + , im'optimalUtilisation = R.reduce 8 10 + } + +-- | Coin configuration +data CoinCfg = CoinCfg + { coinCfg'coin :: Coin + , coinCfg'rate :: Rational + , coinCfg'aToken :: TokenName + , coinCfg'interestModel :: InterestModel + , coinCfg'liquidationBonus :: Rational + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +{-# INLINEABLE initLendingPool #-} +initLendingPool :: CurrencySymbol -> [CoinCfg] -> [UserId] -> [UserId] -> LendingPool +initLendingPool curSym coinCfgs admins oracles = + LendingPool + { lp'reserves = reserves + , lp'users = M.empty + , lp'currency = curSym + , lp'coinMap = coinMap + , lp'healthReport = M.empty + , lp'admins = admins + , lp'trustedOracles = oracles + } + where + reserves = M.fromList $ fmap (\cfg -> (coinCfg'coin cfg, initReserve cfg)) coinCfgs + coinMap = M.fromList $ fmap (\(CoinCfg coin _ aToken _ _) -> (aToken, coin)) coinCfgs + +{-# INLINEABLE initReserve #-} + +-- | Initialise empty reserve with given ratio of its coin to ada +initReserve :: CoinCfg -> Reserve +initReserve CoinCfg {..} = + Reserve + { reserve'wallet = + Wallet + { wallet'deposit = 0 + , wallet'borrow = 0 + , wallet'collateral = 0 + , wallet'scaledBalance = R.fromInteger 0 + } + , reserve'rate = + CoinRate + { coinRate'value = coinCfg'rate + , coinRate'lastUpdateTime = 0 + } + , reserve'liquidationThreshold = R.reduce 8 10 + , reserve'liquidationBonus = coinCfg'liquidationBonus + , reserve'aToken = coinCfg'aToken + , reserve'interest = initInterest coinCfg'interestModel + } + where + initInterest interestModel = + ReserveInterest + { ri'interestModel = interestModel + , ri'liquidityRate = R.fromInteger 0 + , ri'liquidityIndex = R.fromInteger 1 + , ri'normalisedIncome = R.fromInteger 1 + , ri'lastUpdateTime = 0 + } + +-- | User is a set of wallets per currency +data User = User + { user'wallets :: !(Map Coin Wallet) + , user'lastUpdateTime :: !Integer + , user'health :: !Health + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq User where + {-# INLINEABLE (==) #-} + (User ws1 lut1 h1) == (User ws2 lut2 h2) = + and + [ ws1 == ws2 + , lut1 == lut2 + , h1 == h2 + ] + +-- | Health ratio for user per borrow +type Health = Map Coin Rational + +{-# INLINEABLE defaultUser #-} + +-- | Default user with no wallets. +defaultUser :: User +defaultUser = + User + { user'wallets = M.empty + , user'lastUpdateTime = 0 + , user'health = M.empty + } + +{- | Internal walet of the lending app + + All amounts are provided in the currency of the wallet +-} +data Wallet = Wallet + { -- | amount of deposit + wallet'deposit :: !Integer + , -- | amount of collateral + wallet'collateral :: !Integer + , -- | amount of borrow + wallet'borrow :: !Integer + , -- | scaled balance + wallet'scaledBalance :: !Rational + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq Wallet where + {-# INLINEABLE (==) #-} + Wallet d1 c1 b1 sb1 == Wallet d2 c2 b2 sb2 = + and + [ d1 == d2 + , c1 == c2 + , b1 == b2 + , sb1 == sb2 + ] + +{-# INLINEABLE defaultWallet #-} +defaultWallet :: Wallet +defaultWallet = Wallet 0 0 0 (R.fromInteger 0) + +-- | Acts for lending platform +data Act + = -- | user's actions + UserAct + { userAct'time :: Integer + , userAct'userId :: UserId + , userAct'act :: UserAct + } + | -- | price oracle's actions + PriceAct + { priceAct'time :: Integer + , priceAct'userId :: UserId + , priceAct'act :: PriceAct + } + | -- | app admin's actions + GovernAct + { governAct'userd :: UserId + , goverAct'act :: GovernAct + } + | -- | app query actions + QueryAct + { queryAct'userId :: UserId + , queryAct'time :: Integer + , queryAct'act :: QueryAct + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +-- | Lending pool action +data UserAct + = -- | deposit funds + DepositAct + { act'amount :: Integer + , act'asset :: Coin + } + | -- | borrow funds. We have to allocate collateral to be able to borrow + BorrowAct + { act'amount :: Integer + , act'asset :: Coin + , act'rate :: InterestRate + } + | -- | repay part of the borrow + RepayAct + { act'amount :: Integer + , act'asset :: Coin + , act'rate :: InterestRate + } + | -- | swap borrow interest rate strategy (stable to variable) + SwapBorrowRateModelAct + { act'asset :: Coin + , act'rate :: InterestRate + } + | -- | transfer amount of Asset from the user's Wallet to the Contract, locked as the user's Collateral + AddCollateralAct + { add'asset :: Coin + , add'amount :: Integer + } + | -- | transfer amount of Asset from user's Collateral locked in Contract to user's Wallet + RemoveCollateralAct + { remove'asset :: Coin + , remove'amount :: Integer + } + | -- | withdraw funds from deposit + WithdrawAct + { act'asset :: Coin + , act'amount :: Integer + } + | -- | flash loans happen within the single block of transactions + FlashLoanAct -- TODO + | -- | call to liquidate borrows that are unsafe due to health check + -- (see for description) + LiquidationCallAct + { -- | which collateral do we take for borrow repay + act'collateral :: Coin + , -- | identifier of the unhealthy borrow + act'debt :: BadBorrow + , -- | how much of the debt we cover + act'debtToCover :: Integer + , -- | if true, the user receives the aTokens equivalent + -- of the purchased collateral. If false, the user receives + -- the underlying asset directly. + act'receiveAToken :: Bool + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +-- | Query Actions. +data QueryAct + = -- | Query current balance + QueryCurrentBalanceAct () + | -- | Query insolvent accounts + QueryInsolventAccountsAct () + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +-- | Acts that can be done by admin users. +newtype GovernAct + = -- | Adds new reserve + AddReserveAct CoinCfg + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +-- | Updates for the prices of the currencies on the markets +data PriceAct + = -- | Set asset price + SetAssetPriceAct Coin Rational + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +{-# INLINEABLE toLendingToken #-} +toLendingToken :: LendingPool -> Coin -> Maybe Coin +toLendingToken LendingPool {..} coin = + flip fmap (M.lookup coin lp'reserves) $ \Reserve {..} -> AssetClass (lp'currency, reserve'aToken) + +{-# INLINEABLE fromAToken #-} +fromAToken :: LendingPool -> TokenName -> Maybe Coin +fromAToken LendingPool {..} tn = M.lookup tn lp'coinMap + +{-# INLINEABLE fromLendingToken #-} +fromLendingToken :: LendingPool -> Coin -> Maybe Coin +fromLendingToken lp (AssetClass (_, tn)) = fromAToken lp tn + +data InterestRate = StableRate | VariableRate + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +-- | Supported currency of `Reserve` in `LendingPool` +data SupportedCurrency = SupportedCurrency + { -- | underlying + sc'underlying :: !Coin + , -- | aToken + sc'aToken :: !TokenName + , -- | exchange rate + sc'exchangeRate :: !CoinRate + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq SupportedCurrency where + {-# INLINEABLE (==) #-} + SupportedCurrency u1 t1 er1 == SupportedCurrency u2 t2 er2 = + and + [ u1 == u2 + , t1 == t2 + , er1 == er2 + ] + +{- | Query returns the user's funds currently locked in the current Lendex, + including both underlying tokens and aTokens of multiple kinds. Also returns + the user's current borrow amount and advances interest. +-} +data UserBalance = UserBalance + { -- | User Id + ub'id :: !UserId + , -- | Total Deposit for User, + ub'totalDeposit :: !Integer + , -- | Total Collateral for User, + ub'totalCollateral :: !Integer + , -- | Total Borrow for User, + ub'totalBorrow :: !Integer + , -- | Normalised Income for User, + ub'cumulativeBalance :: Map Coin Rational + , -- | User Funds + ub'funds :: Map Coin Wallet + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq UserBalance where + {-# INLINEABLE (==) #-} + (UserBalance id1 td1 tc1 tb1 cb1 f1) == (UserBalance id2 td2 tc2 tb2 cb2 f2) = + and + [ id1 == id2 + , td1 == td2 + , tc1 == tc2 + , tb1 == tb2 + , cb1 == cb2 + , f1 == f2 + ] + +data InsolventAccount = InsolventAccount + { -- | User Id + ia'id :: !UserId + , -- | Insolvent Currencies, with their Current health. + ia'ic :: [(Coin, Rational)] + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq InsolventAccount where + {-# INLINEABLE (==) #-} + (InsolventAccount id1 ic1) == (InsolventAccount id2 ic2) = + and + [ id1 == id2 + , ic1 == ic2 + ] + +-- If anot her query is added, extend this data type + +-- | Results of query endpoints calls on `QueryContract` +data QueryRes + = QueryResAllLendexes [(Address, LendingPool)] + | QueryResSupportedCurrencies {getSupported :: [SupportedCurrency]} + | QueryResCurrentBalance UserBalance + | QueryResInsolventAccounts [InsolventAccount] + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +instance Eq QueryRes where + {-# INLINEABLE (==) #-} + QueryResAllLendexes ls1 == QueryResAllLendexes ls2 = + ls1 == ls2 + QueryResSupportedCurrencies scs1 == QueryResSupportedCurrencies scs2 = + scs1 == scs2 + QueryResCurrentBalance b1 == QueryResCurrentBalance b2 = + b1 == b2 + QueryResInsolventAccounts ias1 == QueryResInsolventAccounts ias2 = + ias1 == ias2 + _ == _ = False + +--------------------------------------------------------------- +-- boilerplate instances + +PlutusTx.unstableMakeIsData ''CoinCfg +PlutusTx.unstableMakeIsData ''CoinRate +PlutusTx.unstableMakeIsData ''InterestModel +PlutusTx.unstableMakeIsData ''InterestRate +PlutusTx.unstableMakeIsData ''ReserveInterest +PlutusTx.unstableMakeIsData ''UserAct +PlutusTx.unstableMakeIsData ''PriceAct +PlutusTx.unstableMakeIsData ''GovernAct +PlutusTx.unstableMakeIsData ''QueryAct +PlutusTx.unstableMakeIsData ''User +PlutusTx.unstableMakeIsData ''Wallet +PlutusTx.unstableMakeIsData ''Reserve +PlutusTx.unstableMakeIsData ''StartParams +PlutusTx.unstableMakeIsData ''BadBorrow +PlutusTx.unstableMakeIsData ''LendingPool +PlutusTx.unstableMakeIsData ''Act +PlutusTx.unstableMakeIsData ''LendexId +PlutusTx.makeLift ''LendexId diff --git a/mlabs/src/Mlabs/NFT/Api.hs b/mlabs/src/Mlabs/NFT/Api.hs new file mode 100644 index 000000000..0aaa01ef7 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Api.hs @@ -0,0 +1,118 @@ +module Mlabs.NFT.Api ( + adminEndpoints, + ApiAdminContract, + ApiUserContract, + endpoints, + NFTAppSchema, + nftMarketUserEndpoints, + queryEndpoints, + schemas, +) where + +import Data.Monoid (Last (..)) +import Data.Text (Text) + +import Control.Monad (void) + +import Playground.Contract (mkSchemaDefinitions) +import Plutus.Contract (Contract, Endpoint, Promise, endpoint, type (.\/)) +import Prelude as Hask + +import Mlabs.NFT.Contract.BidAuction (bidAuction) +import Mlabs.NFT.Contract.Buy (buy) +import Mlabs.NFT.Contract.CloseAuction (closeAuction) +import Mlabs.NFT.Contract.Init (initApp) +import Mlabs.NFT.Contract.Mint (mint) +import Mlabs.NFT.Contract.OpenAuction (openAuction) +import Mlabs.NFT.Contract.Query (queryContent, queryCurrentOwner, queryCurrentPrice, queryListNfts) +import Mlabs.NFT.Contract.SetPrice (setPrice) +import Mlabs.NFT.Types ( + AdminContract, + AuctionBidParams (..), + AuctionCloseParams (..), + AuctionOpenParams (..), + BuyRequestUser (..), + Content, + InitParams (..), + MintParams (..), + NftAppInstance (..), + NftId (..), + SetPriceParams (..), + UniqueToken, + UserContract, + UserWriter, + ) +import Mlabs.Plutus.Contract (selectForever) + +-- | A common App schema works for now. +type NFTAppSchema = + -- Author Endpoint + Endpoint "mint" MintParams + -- User Action Endpoints + .\/ Endpoint "buy" BuyRequestUser + .\/ Endpoint "set-price" SetPriceParams + -- Query Endpoints + .\/ Endpoint "query-current-owner" NftId + .\/ Endpoint "query-current-price" NftId + .\/ Endpoint "query-list-nfts" () + .\/ Endpoint "query-content" Content + -- Auction endpoints + .\/ Endpoint "auction-open" AuctionOpenParams + .\/ Endpoint "auction-bid" AuctionBidParams + .\/ Endpoint "auction-close" AuctionCloseParams + -- Admin Endpoint + .\/ Endpoint "app-init" InitParams + +mkSchemaDefinitions ''NFTAppSchema + +-- ENDPOINTS -- + +type ApiUserContract a = UserContract NFTAppSchema a + +type ApiAdminContract a = AdminContract NFTAppSchema a + +-- | Utility function to create endpoints from promises. +mkEndpoints :: forall w s e a b. (b -> [Promise w s e a]) -> b -> Contract w s e a +mkEndpoints listCont = selectForever . listCont + +-- | User Endpoints . +endpoints :: UniqueToken -> ApiUserContract () +endpoints = mkEndpoints userEndpointsList + +-- | Query Endpoints are used for Querying, with no on-chain tx generation. +queryEndpoints :: UniqueToken -> ApiUserContract () +queryEndpoints = mkEndpoints queryEndpointsList + +-- | Admin Endpoints +adminEndpoints :: ApiAdminContract () +adminEndpoints = mkEndpoints (const adminEndpointsList) () + +-- | Endpoints for NFT marketplace user - combination of user and query endpoints. +nftMarketUserEndpoints :: UniqueToken -> ApiUserContract () +nftMarketUserEndpoints = mkEndpoints (userEndpointsList <> queryEndpointsList) + +-- | List of User Promises. +userEndpointsList :: UniqueToken -> [Promise UserWriter NFTAppSchema Text ()] +userEndpointsList uT = + [ endpoint @"mint" (mint uT) + , endpoint @"buy" (buy uT) + , endpoint @"set-price" (setPrice uT) + , endpoint @"auction-open" (openAuction uT) + , endpoint @"auction-close" (closeAuction uT) + , endpoint @"auction-bid" (bidAuction uT) + ] + +-- | List of Query endpoints. +queryEndpointsList :: UniqueToken -> [Promise UserWriter NFTAppSchema Text ()] +queryEndpointsList uT = + [ endpoint @"query-current-price" (void . queryCurrentPrice uT) + , endpoint @"query-current-owner" (void . queryCurrentOwner uT) + , endpoint @"query-list-nfts" (void . const (queryListNfts uT)) + , endpoint @"query-content" (void . queryContent uT) + ] + +-- | List of admin endpoints. +adminEndpointsList :: [Promise (Last NftAppInstance) NFTAppSchema Text ()] +adminEndpointsList = + [ endpoint @"app-init" initApp + ] diff --git a/mlabs/src/Mlabs/NFT/Contract.hs b/mlabs/src/Mlabs/NFT/Contract.hs new file mode 100644 index 000000000..937a1ad71 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract.hs @@ -0,0 +1,10 @@ +module Mlabs.NFT.Contract ( + module X, +) where + +import Mlabs.NFT.Contract.Aux as X +import Mlabs.NFT.Contract.Buy as X +import Mlabs.NFT.Contract.Init as X +import Mlabs.NFT.Contract.Mint as X +import Mlabs.NFT.Contract.Query as X +import Mlabs.NFT.Contract.SetPrice as X diff --git a/mlabs/src/Mlabs/NFT/Contract/Aux.hs b/mlabs/src/Mlabs/NFT/Contract/Aux.hs new file mode 100644 index 000000000..5448baa44 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/Aux.hs @@ -0,0 +1,332 @@ +module Mlabs.NFT.Contract.Aux ( + entryToPointInfo, + findNft, + fstUtxoAt, + getAddrUtxos, + getAddrValidUtxos, + getApplicationCurrencySymbol, + getDatumsTxsOrdered, + getDatumsTxsOrderedFromAddr, + getGovHead, + getNftAppSymbol, + getNftDatum, + getNftHead, + getScriptAddrUtxos, + getsNftDatum, + getUId, + getUserAddr, + getUserUtxos, + hashData, + serialiseDatum, + toDatum, +) where + +import PlutusTx qualified +import PlutusTx.Prelude hiding (mconcat, (<>)) +import Prelude (mconcat, (<>)) +import Prelude qualified as Hask + +import Control.Lens (filtered, to, traversed, (^.), (^..), _Just, _Right) +import Data.List qualified as L +import Data.Map qualified as Map +import Data.Text (Text, pack) + +import Plutus.ChainIndex.Tx (ChainIndexTx) +import Plutus.Contract (utxosTxOutTxAt) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Value (assetClassValueOf, symbols) + +import Ledger ( + Address, + ChainIndexTxOut, + Datum (..), + TxOutRef, + ciTxOutDatum, + ciTxOutValue, + getDatum, + pubKeyHashAddress, + toTxOut, + txOutValue, + ) +import Ledger.Value as Value (unAssetClass, valueOf) + +import Mlabs.NFT.Governance.Types (GovDatum (gov'list), LList (HeadLList)) +import Mlabs.NFT.Spooky (toSpooky, toSpookyCurrencySymbol, unSpookyAddress, unSpookyAssetClass, unSpookyTokenName) +import Mlabs.NFT.Types ( + Content, + DatumNft (..), + GenericContract, + NftAppInstance, + NftAppSymbol (NftAppSymbol), + NftId, + NftListHead, + PointInfo (PointInfo, pi'CITxO, pi'data), + UniqueToken, + UserId (UserId), + app'symbol, + appInstance'Address, + appInstance'UniqueToken, + getContent, + info'id, + nftTokenName, + node'information, + ) +import Mlabs.NFT.Validation (nftAsset, txScrAddress) +import Mlabs.Plutus.Contract (readDatum') + +getScriptAddrUtxos :: + UniqueToken -> + GenericContract (Map.Map TxOutRef (ChainIndexTxOut, ChainIndexTx)) +getScriptAddrUtxos = utxosTxOutTxAt . unSpookyAddress . txScrAddress + +-- HELPER FUNCTIONS AND CONTRACTS -- + +-- | Convert to Datum +toDatum :: PlutusTx.ToData a => a -> Datum +toDatum = Datum . PlutusTx.toBuiltinData + +-- | Get the current Wallet's publick key. +getUserAddr :: GenericContract Address +getUserAddr = (`pubKeyHashAddress` Nothing) <$> Contract.ownPaymentPubKeyHash + +-- | Get the current wallet's utxos. +getUserUtxos :: GenericContract (Map.Map TxOutRef Ledger.ChainIndexTxOut) +getUserUtxos = getAddrUtxos =<< getUserAddr + +-- | Get the current wallet's userId. +getUId :: GenericContract UserId +getUId = UserId . toSpooky <$> Contract.ownPaymentPubKeyHash + +-- | Get the ChainIndexTxOut at an address. +getAddrUtxos :: Address -> GenericContract (Map.Map TxOutRef ChainIndexTxOut) +getAddrUtxos adr = Map.map fst <$> utxosTxOutTxAt adr + +{- | Get the Head of the List, by filtering away all the utxos that don't + contain the unique token. +-} +getHead :: UniqueToken -> GenericContract (Maybe (PointInfo NftListHead)) +getHead uT = do + utxos <- utxosTxOutTxAt . unSpookyAddress . txScrAddress $ uT + let headUtxos = Map.toList . Map.filter containUniqueToken $ utxos + case headUtxos of + [] -> pure Nothing + [(oRef, (xOut, x))] -> do + case readDatum' @DatumNft xOut of + Just (HeadDatum datum) -> + pure . Just $ PointInfo datum oRef xOut x + _ -> Contract.throwError "Head has corrupted datum!" + _ -> do + Contract.throwError $ + mconcat + [ "This should have not happened! More than one Heads with Unique Tokens." + --, pack . Hask.show . fmap pi'data $ utxos + ] + where + containUniqueToken = (/= 0) . flip assetClassValueOf (unSpookyAssetClass uT) . (^. ciTxOutValue) . fst + +-- | Get the Symbol +getNftAppSymbol :: UniqueToken -> GenericContract NftAppSymbol +getNftAppSymbol uT = do + lHead <- getHead uT + case lHead of + Nothing -> err + Just headInfo -> do + let uTCS = fst . unAssetClass . unSpookyAssetClass $ uT + let val = filter (\x -> x /= uTCS && x /= "") . symbols $ pi'CITxO headInfo ^. ciTxOutValue + case val of + [x] -> pure . NftAppSymbol . toSpooky . toSpookyCurrencySymbol $ x + [] -> Contract.throwError "Could not establish App Symbol. Does it exist in the HEAD?" + _ -> Contract.throwError "Could not establish App Symbol. Too many symbols to distinguish from." + where + err = Contract.throwError "Could not establish App Symbol." + +-- | Get the ChainIndexTxOut at an address. +getAddrValidUtxos :: UniqueToken -> GenericContract (Map.Map TxOutRef (ChainIndexTxOut, ChainIndexTx)) +getAddrValidUtxos ut = do + appSymbol <- getNftAppSymbol ut + Map.filter (validTx appSymbol) <$> utxosTxOutTxAt (unSpookyAddress . txScrAddress $ ut) + where + validTx appSymbol (cIxTxOut, _) = elem (app'symbol appSymbol) (fmap toSpookyCurrencySymbol (symbols (cIxTxOut ^. ciTxOutValue))) + +-- | Serialise Datum +serialiseDatum :: PlutusTx.ToData a => a -> Datum +serialiseDatum = Datum . PlutusTx.toBuiltinData + +-- | Returns the Datum of a specific nftId from the Script address. +getNftDatum :: NftId -> UniqueToken -> GenericContract (Maybe DatumNft) +getNftDatum nftId ut = do + utxos :: [Ledger.ChainIndexTxOut] <- fmap fst . Map.elems <$> getAddrValidUtxos ut + let datums :: [DatumNft] = + utxos + ^.. traversed . Ledger.ciTxOutDatum + . _Right + . to (PlutusTx.fromBuiltinData @DatumNft . getDatum) + . _Just + . filtered + ( \case + HeadDatum _ -> False + NodeDatum node -> + let nftId' = info'id . node'information $ node + in nftId' == nftId + ) + Contract.logInfo @Hask.String $ Hask.show $ "Datum Found:" <> Hask.show datums + Contract.logInfo @Hask.String $ Hask.show $ "Datum length:" <> Hask.show (Hask.length datums) + case datums of + [x] -> + pure $ Just x + [] -> do + Contract.logError @Hask.String "No suitable Datum can be found." + pure Nothing + _ : _ -> do + Contract.logError @Hask.String "More than one suitable Datum can be found. This should never happen." + pure Nothing + +{- | Gets the Datum of a specific nftId from the Script address, and applies an + extraction function to it. +-} +getsNftDatum :: (DatumNft -> b) -> NftId -> UniqueToken -> GenericContract (Maybe b) +getsNftDatum f nftId = fmap (fmap f) . getNftDatum nftId + +-- | Find NFTs at a specific Address. Will throw an error if none or many are found. +findNft :: NftId -> UniqueToken -> GenericContract (PointInfo DatumNft) +findNft nftId ut = do + utxos <- getAddrValidUtxos ut + case findData utxos of + [v] -> do + Contract.logInfo @Hask.String $ Hask.show $ "findNft: NFT Found:" <> Hask.show v + pure $ pointInfo v + [] -> Contract.throwError $ "findNft: DatumNft not found for " <> (pack . Hask.show) nftId + _ -> + Contract.throwError $ + "Should not happen! More than one DatumNft found for " + <> (pack . Hask.show) nftId + where + findData = + L.filter hasCorrectNft -- filter only datums with desired NftId + . mapMaybe readTxData -- map to Maybe (TxOutRef, ChainIndexTxOut, DatumNft) + . Map.toList + + readTxData (oref, (ciTxOut, ciTx)) = (oref,ciTxOut,,ciTx) <$> readDatum' ciTxOut + + hasCorrectNft (_, ciTxOut, datum, _) = + let (cs, tn) = unAssetClass . unSpookyAssetClass $ nftAsset datum + in tn == (unSpookyTokenName . nftTokenName $ datum) -- sanity check + && case datum of + NodeDatum datum' -> + (info'id . node'information $ datum') == nftId -- check that Datum has correct NftId + && valueOf (ciTxOut ^. ciTxOutValue) cs tn == 1 -- check that UTXO has single NFT in Value + HeadDatum _ -> False + pointInfo (oR, cIxO, d, cIx) = PointInfo d oR cIxO cIx + +-- | Get first utxo at address. Will throw an error if no utxo can be found. +fstUtxoAt :: Address -> GenericContract (TxOutRef, ChainIndexTxOut) +fstUtxoAt address = do + utxos <- Contract.utxosAt address + case Map.toList utxos of + [] -> Contract.throwError @Text "No utxo found at address." + x : _ -> pure x + +-- | Get the Head of the NFT List +getNftHead :: UniqueToken -> GenericContract (Maybe (PointInfo DatumNft)) +getNftHead ut = do + headX <- filter (isHead . pi'data) <$> getDatumsTxsOrdered ut + case headX of + [] -> pure Nothing + [x] -> pure $ Just x + _ -> do + utxos <- getDatumsTxsOrdered @DatumNft ut + Contract.throwError $ + mconcat + [ "This should have not happened! More than one Head Datums. Datums are: " + , pack . Hask.show . fmap pi'data $ utxos + ] + where + isHead = \case + HeadDatum _ -> True + NodeDatum _ -> False + +-- | Get the Head of the Gov List +getGovHead :: Address -> GenericContract (Maybe (PointInfo GovDatum)) +getGovHead addr = do + headX <- filter f <$> getDatumsTxsOrderedFromAddr @GovDatum addr + case headX of + [] -> pure Nothing + [x] -> pure $ Just x + _ -> do + utxos <- getDatumsTxsOrderedFromAddr @GovDatum addr + Contract.throwError $ + mconcat + [ "This should have not happened! More than one Head Datums. Datums are: " + , pack . Hask.show . fmap pi'data $ utxos + ] + where + f = isHead . gov'list . pi'data + + isHead = \case + HeadLList {} -> True + _ -> False + +entryToPointInfo :: + (PlutusTx.FromData a) => + (TxOutRef, (ChainIndexTxOut, ChainIndexTx)) -> + GenericContract (PointInfo a) +entryToPointInfo (oref, (out, tx)) = case readDatum' out of + Nothing -> Contract.throwError "entryToPointInfo: Datum not found" + Just d -> pure $ PointInfo d oref out tx + +{- | Get `DatumNft` together with`TxOutRef` and `ChainIndexTxOut` + for particular `NftAppSymbol` and return them sorted by `DatumNft`'s `Pointer`: + head node first, list nodes ordered by pointer +-} +getDatumsTxsOrdered :: + forall a. + (PlutusTx.FromData a, Ord a, Hask.Eq a) => + UniqueToken -> + GenericContract [PointInfo a] +getDatumsTxsOrdered ut = do + utxos <- Map.toList <$> getAddrValidUtxos ut + let datums = mapMaybe toPointInfo utxos + let sortedDatums = L.sort datums + case sortedDatums of + [] -> Contract.throwError "getDatumsTxsOrdered: Datum not found" + ds -> return ds + where + toPointInfo (oref, (out, tx)) = case readDatum' @a out of + Nothing -> Nothing + Just d -> pure $ PointInfo d oref out tx + +getDatumsTxsOrderedFromAddr :: + forall a. + (PlutusTx.FromData a, Ord a, Hask.Eq a) => + Address -> + GenericContract [PointInfo a] +getDatumsTxsOrderedFromAddr addr = do + utxos <- Map.toList <$> utxosTxOutTxAt addr + let datums = mapMaybe toPointInfo utxos + let sortedDatums = L.sort datums + case sortedDatums of + [] -> Contract.throwError "getDatumsTxsOrderedFromAddr: Datum not found" + ds -> return ds + where + toPointInfo (oref, (out, tx)) = case readDatum' @a out of + Nothing -> Nothing + Just d -> pure $ PointInfo d oref out tx + +-- | A hashing function to minimise the data to be attached to the NTFid. +hashData :: Content -> BuiltinByteString +hashData = sha2_256 . getContent + +getApplicationCurrencySymbol :: NftAppInstance -> GenericContract NftAppSymbol +getApplicationCurrencySymbol appInstance = do + utxos <- Contract.utxosAt . unSpookyAddress . appInstance'Address $ appInstance + let outs = fmap toTxOut . Map.elems $ utxos + (uniqueCurrency, uniqueToken) = unAssetClass . unSpookyAssetClass . appInstance'UniqueToken $ appInstance + lstHead' = find (\tx -> valueOf (Ledger.txOutValue tx) uniqueCurrency uniqueToken == 1) outs + headUtxo <- case lstHead' of + Nothing -> Contract.throwError "Head not found" + Just lstHead -> pure lstHead + let currencies = filter (uniqueCurrency /=) $ symbols . Ledger.txOutValue $ headUtxo + case currencies of + [appSymbol] -> pure . NftAppSymbol . toSpooky . toSpookyCurrencySymbol $ appSymbol + [] -> Contract.throwError "Head does not contain AppSymbol" + _ -> Contract.throwError "Head contains more than 2 currencies (Unreachable?)" diff --git a/mlabs/src/Mlabs/NFT/Contract/BidAuction.hs b/mlabs/src/Mlabs/NFT/Contract/BidAuction.hs new file mode 100644 index 000000000..624732537 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/BidAuction.hs @@ -0,0 +1,129 @@ +{-# LANGUAGE UndecidableInstances #-} +-- FIXME: Remove after uncommenting commented parts +{-# OPTIONS_GHC -Wno-unused-imports #-} + +module Mlabs.NFT.Contract.BidAuction ( + bidAuction, +) where + +import PlutusTx.Prelude hiding (mconcat, mempty, unless, (<>)) +import Prelude (mconcat, (<>)) +import Prelude qualified as Hask + +import Control.Monad (void, when) +import Data.Map qualified as Map +import Data.Monoid (Last (..)) +import Data.Text (Text) +import Text.Printf (printf) + +import Plutus.ChainIndex.Tx (txOutRefMapForAddr) +import Plutus.Contract (Contract) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import PlutusTx qualified + +import Ledger ( + Datum (..), + Redeemer (..), + to, + txOutValue, + ) + +import Ledger.Constraints qualified as Constraints +import Ledger.Typed.Scripts (validatorScript) +import Plutus.V1.Ledger.Ada qualified as Ada + +import Mlabs.NFT.Contract.Aux +import Mlabs.NFT.Spooky (toSpooky) +import Mlabs.NFT.Types +import Mlabs.NFT.Validation + +{- | + Attempts to bid on NFT auction, locks new bid in the script, returns previous bid to previous bidder, + and sets new bid for the NFT. +-} +bidAuction :: UniqueToken -> AuctionBidParams -> Contract UserWriter s Text () +bidAuction _ _ = + error () + +-- bidAuction uT (AuctionBidParams nftId bidAmount) = do +-- ownOrefTxOut <- getUserAddr >>= fstUtxoAt +-- ownPkh <- Contract.ownPubKeyHash +-- PointInfo {..} <- findNft nftId uT +-- node <- case pi'data of +-- NodeDatum n -> Hask.pure n +-- _ -> Contract.throwError "NFT not found" + +-- let mauctionState = info'auctionState . node'information $ node +-- when (isNothing mauctionState) $ Contract.throwError "Can't bid: no auction in progress" +-- auctionState <- maybe (Contract.throwError "No auction state when expected") pure mauctionState +-- case as'highestBid auctionState of +-- Nothing -> +-- when (bidAmount < as'minBid auctionState) (Contract.throwError "Auction bid lower than minimal bid") +-- Just (AuctionBid bid _) -> +-- when (bidAmount < bid) (Contract.throwError "Auction bid lower than previous bid") + +-- userUtxos <- getUserUtxos +-- symbol <- getNftAppSymbol uT + +-- let newHighestBid = +-- AuctionBid +-- { ab'bid' = toSpooky bidAmount +-- , ab'bidder' = toSpooky $ UserId ownPkh +-- } +-- newAuctionState = +-- auctionState {as'highestBid' = toSpooky $ Just newHighestBid} + +-- nftDatum = NodeDatum $ updateDatum newAuctionState node +-- scriptAddr = appInstance'Address . node'appInstance $ node +-- prevVal = Ada.lovelaceValueOf $ case as'highestBid auctionState of +-- Nothing -> 0 +-- Just (AuctionBid bid _) -> - bid +-- nftVal = +-- (prevVal <>) +-- . Ledger.txOutValue +-- . fst +-- $ (txOutRefMapForAddr scriptAddr pi'CITx Map.! pi'TOR) +-- action = +-- BidAuctionAct +-- { act'bid = bidAmount +-- , act'symbol = symbol +-- } +-- lookups = +-- mconcat +-- [ Constraints.unspentOutputs userUtxos +-- , Constraints.unspentOutputs $ Map.fromList [ownOrefTxOut] +-- , Constraints.unspentOutputs $ Map.fromList [(pi'TOR, pi'CITxO)] +-- , Constraints.typedValidatorLookups (txPolicy uT) +-- , Constraints.otherScript (validatorScript $ txPolicy uT) +-- ] + +-- bidDependentTxConstraints = +-- case as'highestBid auctionState of +-- Nothing -> [] +-- Just (AuctionBid bid bidder) -> +-- [ Constraints.mustPayToPubKey (getUserId bidder) (Ada.lovelaceValueOf bid) +-- ] +-- tx = +-- mconcat +-- ( [ Constraints.mustPayToTheScript (toBuiltinData nftDatum) (nftVal <> Ada.lovelaceValueOf bidAmount) +-- , Constraints.mustIncludeDatum (Datum . PlutusTx.toBuiltinData $ nftDatum) +-- , Constraints.mustSpendPubKeyOutput (fst ownOrefTxOut) +-- , Constraints.mustSpendScriptOutput +-- pi'TOR +-- (Redeemer . PlutusTx.toBuiltinData $ action) +-- , Constraints.mustValidateIn (to $ as'deadline auctionState) +-- ] +-- ++ bidDependentTxConstraints +-- ) +-- void $ Contract.submitTxConstraintsWith lookups tx +-- Contract.tell . Last . Just . Left $ nftId +-- void $ Contract.logInfo @Hask.String $ printf "Bidding %s in auction for %s" (Hask.show bidAmount) (Hask.show nftVal) +-- where +-- updateDatum newAuctionState node = +-- node +-- { node'information = +-- (node'information node) +-- { info'auctionState = Just newAuctionState +-- } +-- } diff --git a/mlabs/src/Mlabs/NFT/Contract/Buy.hs b/mlabs/src/Mlabs/NFT/Contract/Buy.hs new file mode 100644 index 000000000..3fbd51470 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/Buy.hs @@ -0,0 +1,129 @@ +{-# LANGUAGE UndecidableInstances #-} + +module Mlabs.NFT.Contract.Buy ( + buy, +) where + +import PlutusTx qualified +import PlutusTx.Prelude hiding (mconcat, (<>)) +import Prelude (mconcat) +import Prelude qualified as Hask + +import Control.Lens ((^.)) +import Control.Monad (void, when) +import Data.Map qualified as Map +import Data.Monoid (Last (..), (<>)) +import Data.Text (Text) + +import Ledger ( + Datum (..), + Redeemer (..), + ciTxOutValue, + ) +import Ledger.Typed.Scripts (validatorScript) +import Plutus.Contract (Contract) +import Plutus.Contract qualified as Contract +import Plutus.Contract.Constraints qualified as Constraints +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) + +import Mlabs.NFT.Contract.Aux ( + findNft, + fstUtxoAt, + getNftAppSymbol, + getUId, + getUserAddr, + getUserUtxos, + ) +import Mlabs.NFT.Contract.Gov.Fees (getFeesConstraints) +import Mlabs.NFT.Contract.Gov.Query (queryCurrFeeRate) +import Mlabs.NFT.Spooky (toSpooky, unSpookyPaymentPubKeyHash, unSpookyValue) +import Mlabs.NFT.Types ( + BuyRequestUser (..), + DatumNft (NodeDatum), + InformationNft (info'owner', info'price'), + NftListNode (node'information'), + PointInfo (pi'CITxO, pi'TOR, pi'data), + UniqueToken, + UserAct (BuyAct, act'bid', act'newPrice', act'symbol'), + UserId (UserId), + UserWriter, + getUserId, + info'author, + info'owner, + info'price, + info'share, + node'information, + ) +import Mlabs.NFT.Validation (calculateShares, txPolicy) + +{- | BUY. + Attempts to buy a new NFT by changing the owner, pays the current owner and + the author, and sets a new price for the NFT. +-} +buy :: forall s. UniqueToken -> BuyRequestUser -> Contract UserWriter s Text () +buy uT BuyRequestUser {..} = do + ownOrefTxOut <- getUserAddr >>= fstUtxoAt + ownPkh <- Contract.ownPaymentPubKeyHash + nftPi <- findNft ur'nftId uT + node <- case pi'data nftPi of + NodeDatum n -> Hask.pure n + _ -> Contract.throwError "NFT not found" + price <- case info'price . node'information $ node of + Nothing -> Contract.throwError "NFT not for sale." + Just price -> Hask.pure price + + when (ur'price < price) $ + Contract.throwError "Bid price is too low." + + userUtxos <- getUserUtxos + feeRate <- queryCurrFeeRate uT + + user <- getUId + (govTx, govLookups) <- getFeesConstraints uT ur'nftId ur'price user + symbol <- getNftAppSymbol uT + + let feeValue = round $ fromInteger ur'price * feeRate + (paidToOwner, paidToAuthor) = + calculateShares (ur'price - feeValue) . info'share . node'information $ node + nftDatum = NodeDatum $ updateNftDatum (toSpooky ownPkh) node + nftVal = pi'CITxO nftPi ^. ciTxOutValue + action = + BuyAct + { act'bid' = toSpooky ur'price + , act'newPrice' = toSpooky ur'newPrice + , act'symbol' = toSpooky symbol + } + lookups = + mconcat $ + [ Constraints.unspentOutputs userUtxos + , Constraints.unspentOutputs $ Map.fromList [ownOrefTxOut] + , Constraints.unspentOutputs $ Map.fromList [(pi'TOR nftPi, pi'CITxO nftPi)] + , Constraints.typedValidatorLookups (txPolicy uT) + , Constraints.otherScript (validatorScript $ txPolicy uT) + ] + <> govLookups + tx = + mconcat $ + [ Constraints.mustPayToTheScript (toBuiltinData nftDatum) nftVal + , Constraints.mustIncludeDatum (Datum . PlutusTx.toBuiltinData $ nftDatum) + , Constraints.mustPayToPubKey (unSpookyPaymentPubKeyHash . getUserId . info'author . node'information $ node) (unSpookyValue paidToAuthor) + , Constraints.mustPayToPubKey (unSpookyPaymentPubKeyHash . getUserId . info'owner . node'information $ node) (unSpookyValue paidToOwner) + , Constraints.mustSpendPubKeyOutput (fst ownOrefTxOut) + , Constraints.mustSpendScriptOutput + (pi'TOR nftPi) + (Redeemer . PlutusTx.toBuiltinData $ action) + ] + <> govTx + void $ Contract.submitTxConstraintsWith lookups tx + Contract.tell . Last . Just . Left $ ur'nftId + Contract.logInfo @Hask.String "buy successful!" + where + updateNftDatum newOwner node = + node + { node'information' = + toSpooky + (node'information node) + { info'price' = toSpooky ur'newPrice + , info'owner' = toSpooky $ UserId newOwner + } + } diff --git a/mlabs/src/Mlabs/NFT/Contract/CloseAuction.hs b/mlabs/src/Mlabs/NFT/Contract/CloseAuction.hs new file mode 100644 index 000000000..0df7076aa --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/CloseAuction.hs @@ -0,0 +1,129 @@ +{-# LANGUAGE UndecidableInstances #-} +-- FIXME: Remove after uncommenting commented parts +{-# OPTIONS_GHC -Wno-unused-imports #-} + +module Mlabs.NFT.Contract.CloseAuction ( + closeAuction, +) where + +import PlutusTx.Prelude hiding (mconcat, mempty, unless, (<>)) +import Prelude (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Map qualified as Map +import Data.Monoid (Last (..), (<>)) +import Data.Text (Text) +import Text.Printf (printf) + +import Plutus.Contract (Contract) +import Plutus.Contract qualified as Contract +import PlutusTx qualified + +import Ledger ( + Datum (..), + Redeemer (..), + from, + ) + +import Ledger.Constraints qualified as Constraints +import Ledger.Typed.Scripts (validatorScript) +import Ledger.Value qualified as Value +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) + +import Mlabs.NFT.Contract.Aux +import Mlabs.NFT.Contract.Gov.Fees +import Mlabs.NFT.Contract.Gov.Query +import Mlabs.NFT.Spooky (toSpooky) +import Mlabs.NFT.Types +import Mlabs.NFT.Validation +import Plutus.Contracts.Auction (auctionBuyer) + +{- | + Attempts to close NFT auction, checks if owner is closing an auction and deadline passed, + pays from script to previous owner, and sets new owner. +-} +closeAuction :: UniqueToken -> AuctionCloseParams -> Contract UserWriter s Text () +closeAuction _ _ = + error () + +-- closeAuction uT (AuctionCloseParams nftId) = do +-- ownOrefTxOut <- getUserAddr >>= fstUtxoAt +-- PointInfo {..} <- findNft nftId uT +-- node <- case pi'data of +-- NodeDatum n -> Hask.pure n +-- _ -> Contract.throwError "NFT not found" + +-- let mauctionState = info'auctionState . node'information $ node + +-- auctionState <- maybe (Contract.throwError "Can't close: no auction in progress") pure mauctionState + +-- userUtxos <- getUserUtxos +-- (bidDependentTxConstraints, bidDependentLookupConstraints) <- getBidDependentConstraints auctionState node + +-- symbol <- getNftAppSymbol uT + +-- let newOwner = case as'highestBid auctionState of +-- Nothing -> info'owner . node'information $ node +-- Just (AuctionBid _ bidder) -> bidder + +-- nftDatum = NodeDatum $ updateDatum newOwner node +-- nftVal = Value.singleton (app'symbol symbol) (Value.TokenName . nftId'contentHash $ nftId) 1 +-- action = +-- CloseAuctionAct +-- { act'symbol' = toSpooky symbol +-- } +-- lookups = +-- mconcat $ +-- [ Constraints.unspentOutputs userUtxos +-- , Constraints.unspentOutputs $ Map.fromList [ownOrefTxOut] +-- , Constraints.unspentOutputs $ Map.fromList [(pi'TOR, pi'CITxO)] +-- , Constraints.typedValidatorLookups (txPolicy uT) +-- , Constraints.otherScript (validatorScript $ txPolicy uT) +-- ] +-- <> bidDependentLookupConstraints + +-- tx = +-- mconcat $ +-- [ Constraints.mustPayToTheScript (toBuiltinData nftDatum) nftVal +-- , Constraints.mustIncludeDatum (Datum . PlutusTx.toBuiltinData $ nftDatum) +-- , Constraints.mustSpendPubKeyOutput (fst ownOrefTxOut) +-- , Constraints.mustSpendScriptOutput +-- pi'TOR +-- (Redeemer . PlutusTx.toBuiltinData $ action) +-- , Constraints.mustValidateIn (from $ as'deadline auctionState) +-- ] +-- <> bidDependentTxConstraints + +-- void $ Contract.submitTxConstraintsWith lookups tx +-- Contract.tell . Last . Just . Left $ nftId +-- void $ Contract.logInfo @Hask.String $ printf "Closing auction for %s" $ Hask.show nftVal +-- where +-- updateDatum newOwner node = +-- node +-- { node'information' = toSpooky $ +-- (node'information node) +-- { info'owner' = toSpooky newOwner +-- , info'auctionState' = toSpooky Nothing +-- } +-- } + +-- -- If someone bid on auction, returns constrains to pay to owner, author, mint GOV, and pay fees +-- getBidDependentConstraints auctionState node = case as'highestBid auctionState of +-- Nothing -> Hask.pure ([], []) +-- Just auctionBid -> do +-- feeRate <- queryCurrFeeRate uT +-- let bid = ab'bid auctionBid +-- feeValue = round $ fromInteger bid * feeRate +-- (amountPaidToOwner, amountPaidToAuthor) = +-- calculateShares (bid - feeValue) (info'share . node'information $ node) +-- payTx = +-- [ Constraints.mustPayToPubKey +-- (getUserId . info'owner . node'information $ node) +-- amountPaidToOwner +-- , Constraints.mustPayToPubKey +-- (getUserId . info'author . node'information $ node) +-- amountPaidToAuthor +-- ] +-- (govTx, govLookups) <- getFeesConstraints uT nftId bid bidder' +-- Hask.pure (govTx <> payTx, govLookups) diff --git a/mlabs/src/Mlabs/NFT/Contract/Gov.hs b/mlabs/src/Mlabs/NFT/Contract/Gov.hs new file mode 100644 index 000000000..654375562 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/Gov.hs @@ -0,0 +1 @@ +module Mlabs.NFT.Contract.Gov () where diff --git a/mlabs/src/Mlabs/NFT/Contract/Gov/Aux.hs b/mlabs/src/Mlabs/NFT/Contract/Gov/Aux.hs new file mode 100644 index 000000000..002b5b62e --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/Gov/Aux.hs @@ -0,0 +1,35 @@ +module Mlabs.NFT.Contract.Gov.Aux ( + pointNodeTo', + pointNodeToMaybe', + piValue, +) where + +import Data.Map qualified as Map +import Data.Maybe (fromJust) + +import Plutus.ChainIndex.Tx (txOutRefMapForAddr) +import PlutusTx.Prelude hiding (mconcat, mempty, (<>)) + +import Ledger ( + Address, + Value, + txOutValue, + ) + +import Mlabs.Data.LinkedList +import Mlabs.NFT.Governance.Types +import Mlabs.NFT.Types + +-- `fromJust` is safe here due to on-chain constraints. +pointNodeTo' :: GovDatum -> GovDatum -> GovDatum +pointNodeTo' a b = GovDatum . fromJust $ pointNodeTo (gov'list a) (gov'list b) + +pointNodeToMaybe' :: GovDatum -> Maybe GovDatum -> GovDatum +pointNodeToMaybe' a b = GovDatum . fromJust $ pointNodeToMaybe (gov'list a) (fmap gov'list b) + +-- Get value attached to given `PointInfo` +piValue :: Address -> PointInfo a -> Value +piValue govAddr pi = + Ledger.txOutValue + . fst + $ (txOutRefMapForAddr govAddr (pi'CITx pi) Map.! pi'TOR pi) diff --git a/mlabs/src/Mlabs/NFT/Contract/Gov/Fees.hs b/mlabs/src/Mlabs/NFT/Contract/Gov/Fees.hs new file mode 100644 index 000000000..008fa1e8c --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/Gov/Fees.hs @@ -0,0 +1,156 @@ +module Mlabs.NFT.Contract.Gov.Fees ( + getFeesConstraints, +) where + +import Prelude qualified as Hask + +import Data.Map qualified as Map +import Data.Monoid ((<>)) +import Data.Text (Text) + +import Plutus.ChainIndex.Tx (txOutRefMapForAddr) +import Plutus.Contract (Contract) +import Plutus.Contract qualified as Contract +import Plutus.Contract.Constraints qualified as Constraints +import Plutus.V1.Ledger.Ada qualified as Ada ( + lovelaceValueOf, + ) +import PlutusTx.Prelude hiding (mconcat, mempty, (<>)) + +import Ledger ( + Address, + scriptCurrencySymbol, + txOutValue, + ) +import Ledger.Typed.Scripts (Any, validatorHash, validatorScript) +import Ledger.Value as Value (TokenName (..), singleton) + +import Mlabs.Data.LinkedList +import Mlabs.NFT.Contract.Aux +import Mlabs.NFT.Contract.Gov.Aux +import Mlabs.NFT.Contract.Gov.Query +import Mlabs.NFT.Governance.Types +import Mlabs.NFT.Governance.Validation (govMintPolicy, govScript) +import Mlabs.NFT.Spooky +import Mlabs.NFT.Types +import Mlabs.NFT.Validation + +-- | Returns constraints for minting GOV tokens, and paying transaction fee for given NFT +getFeesConstraints :: + forall s. + UniqueToken -> + NftId -> + Integer -> + UserId -> + Contract UserWriter s Text ([Constraints.TxConstraints BuiltinData BuiltinData], [Constraints.ScriptLookups Any]) +getFeesConstraints uT nftId price user = do + let ownPkh = getUserId user + nftPi <- findNft nftId uT + node <- case pi'data nftPi of + NodeDatum n -> Hask.pure n + _ -> Contract.throwError "getFeesConstraints:NFT not found" + let newGovDatum = GovDatum $ NodeLList user GovLNode Nothing + govAddr = unSpookyAddress . appInstance'Governance . node'appInstance $ node + govValidator = govScript . appInstance'UniqueToken . node'appInstance $ node + govScriptHash = validatorHash govValidator + + feeRate <- queryCurrFeeRate uT + feePkh <- PaymentPubKeyHash . toSpooky . toSpookyPubKeyHash <$> queryFeePkh uT + govHead' <- getGovHead govAddr + govHead <- case govHead' of + Just x -> Hask.pure x + Nothing -> Contract.throwError "getFeesConstraints: GOV HEAD not found" + + govPi' <- findGovInsertPoint govAddr newGovDatum + Contract.logInfo @Hask.String $ Hask.show $ "Gov found: " <> Hask.show govPi' + let feeValue = round $ fromInteger price * feeRate + mkGov name = + Value.singleton + (scriptCurrencySymbol govPolicy) + (Value.TokenName . (name <>) . getPubKeyHash . unPaymentPubKeyHash $ ownPkh) + feeValue + mintedFreeGov = mkGov "freeGov" + mintedListGov = mkGov "listGov" + appInstance = node'appInstance node + govPolicy = govMintPolicy appInstance + govRedeemer = asRedeemer MintGov + headPrevValue = piValue govAddr govHead + spendHead = + Hask.mconcat + [ Constraints.mustSpendScriptOutput (pi'TOR govHead) govRedeemer + , Constraints.mustPayToOtherScript + govScriptHash + (toDatum $ pi'data govHead) + headPrevValue + ] + sharedGovTx = + [ Constraints.mustMintValueWithRedeemer govRedeemer (mintedFreeGov <> mintedListGov) + , Constraints.mustPayToPubKey (unSpookyPaymentPubKeyHash ownPkh) mintedFreeGov + , Constraints.mustPayToPubKey (unSpookyPaymentPubKeyHash feePkh) (Ada.lovelaceValueOf feeValue) + ] + sharedGovLookup = + [ Constraints.mintingPolicy govPolicy + , Constraints.otherScript (validatorScript govValidator) + , Constraints.unspentOutputs $ Map.singleton (pi'TOR govHead) (pi'CITxO govHead) + ] + govTx = case govPi' of + -- Updating already existing Node + Left govPi -> + let prevValue = + Ledger.txOutValue + . fst + $ (txOutRefMapForAddr govAddr (pi'CITx govPi) Map.! pi'TOR govPi) + in [ -- Send more GOV tokens to existing Node + Constraints.mustSpendScriptOutput (pi'TOR govPi) govRedeemer + , Constraints.mustPayToOtherScript + govScriptHash + (toDatum . pi'data $ govPi) + (mintedListGov <> prevValue) + , spendHead + ] + -- Inserting new Node + Right govIp -> + let updatedNewNode = pointNodeToMaybe' newGovDatum (fmap pi'data . next $ govIp) + updatedPrevNode = pointNodeTo' (pi'data . prev $ govIp) updatedNewNode + prevValue = + Ledger.txOutValue + . fst + $ (txOutRefMapForAddr govAddr (pi'CITx . prev $ govIp) Map.! (pi'TOR . prev $ govIp)) + in [ Constraints.mustSpendScriptOutput (pi'TOR . prev $ govIp) govRedeemer + , Constraints.mustPayToOtherScript + govScriptHash + (toDatum updatedPrevNode) + prevValue + , case gov'list updatedPrevNode of + HeadLList {} -> Hask.mempty + NodeLList {} -> spendHead + , -- Create new Node + Constraints.mustPayToOtherScript + govScriptHash + (toDatum updatedNewNode) + mintedListGov + ] + govLookups = case govPi' of + Left govPi -> + [ Constraints.unspentOutputs $ Map.singleton (pi'TOR govPi) (pi'CITxO govPi) + ] + Right govIp -> + [ Constraints.unspentOutputs $ Map.singleton (pi'TOR . prev $ govIp) (pi'CITxO . prev $ govIp) + ] + Hask.pure (govTx <> sharedGovTx, govLookups <> sharedGovLookup) + +findGovInsertPoint :: Ledger.Address -> GovDatum -> GenericContract (Either (PointInfo GovDatum) (InsertPoint GovDatum)) +findGovInsertPoint addr node = do + list <- getDatumsTxsOrderedFromAddr @GovDatum addr + Contract.logInfo @Hask.String $ Hask.show $ "GOV LIST: " <> Hask.show (Hask.fmap pi'data list) + case list of + [] -> Contract.throwError "This should never happen." -- Unreachable + x : xs -> findPoint x xs + where + findPoint x = \case + [] -> pure $ Right $ InsertPoint x Nothing + (y : ys) -> + case Hask.compare (pi'data y) node of + LT -> findPoint y ys + EQ -> pure $ Left y + GT -> pure $ Right $ InsertPoint x (Just y) diff --git a/mlabs/src/Mlabs/NFT/Contract/Gov/Query.hs b/mlabs/src/Mlabs/NFT/Contract/Gov/Query.hs new file mode 100644 index 000000000..756c2a008 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/Gov/Query.hs @@ -0,0 +1,88 @@ +module Mlabs.NFT.Contract.Gov.Query ( + querryCurrentStake, + queryCurrFeeRate, + queryFeePkh, +) where + +import Prelude qualified as Hask + +import Data.Monoid ((<>)) +import Data.Text (Text) + +import Plutus.Contract (Contract) +import Plutus.Contract qualified as Contract +import PlutusTx.Prelude hiding (mconcat, mempty, (<>)) + +import Ledger ( + getPubKeyHash, + scriptCurrencySymbol, + ) +import Ledger qualified +import Ledger.Value as Value (TokenName (..), valueOf) + +import Mlabs.Data.LinkedList +import Mlabs.NFT.Contract.Aux +import Mlabs.NFT.Contract.Gov.Aux +import Mlabs.NFT.Governance.Types +import Mlabs.NFT.Governance.Validation +import Mlabs.NFT.Spooky (unPaymentPubKeyHash, unSpookyAddress, unSpookyPubKeyHash) +import Mlabs.NFT.Types + +-- | Returns current `listGov` stake for user +querryCurrentStake :: + forall s. + UniqueToken -> + () -> + Contract UserWriter s Text Integer +querryCurrentStake uT _ = do + user <- getUId + nftHead' <- getNftHead uT + nftHead <- case nftHead' of + Just (PointInfo (HeadDatum x) _ _ _) -> Hask.pure x + _ -> Contract.throwError "queryCurrentStake: NFT HEAD not found" + let ownPkh = getUserId user + listGovTokenName = TokenName . ("listGov" <>) . getPubKeyHash . unSpookyPubKeyHash $ unPaymentPubKeyHash ownPkh + newGovDatum = GovDatum $ NodeLList user GovLNode Nothing + appInstance = head'appInstance nftHead + govAddr = unSpookyAddress . appInstance'Governance $ appInstance + govCurr = scriptCurrencySymbol govPolicy + govPolicy = govMintPolicy appInstance + currGov <- findGov govAddr newGovDatum + let nodeValue = piValue govAddr currGov + Hask.pure $ valueOf nodeValue govCurr listGovTokenName + where + findGov addr node = do + list <- getDatumsTxsOrderedFromAddr @GovDatum addr + findPoint list + where + findPoint = \case + (x1 : xs) -> + if pi'data x1 == node + then pure x1 + else findPoint xs + _ -> Contract.throwError "GOV node not found" + +queryGovHeadDatum :: forall w s. UniqueToken -> Contract w s Text GovLHead +queryGovHeadDatum uT = do + nftHead' <- getNftHead uT + nftHead <- case pi'data <$> nftHead' of + Just (HeadDatum x) -> Hask.pure x + _ -> Contract.throwError "queryCurrFeeRate: NFT HEAD not found" + + let govAddr = unSpookyAddress . appInstance'Governance . head'appInstance $ nftHead + govHead' <- getGovHead govAddr + case gov'list . pi'data <$> govHead' of + Just (HeadLList x _) -> Hask.pure x + _ -> Contract.throwError "queryCurrFeeRate: GOV HEAD not found" + +-- | Get fee rate from GOV HEAD +queryCurrFeeRate :: forall w s. UniqueToken -> Contract w s Text Rational +queryCurrFeeRate uT = do + govHead <- queryGovHeadDatum uT + Hask.pure $ govLHead'feeRate govHead + +-- | Get fee pkh from GOV HEAD +queryFeePkh :: forall w s. UniqueToken -> Contract w s Text Ledger.PubKeyHash +queryFeePkh uT = do + govHead <- queryGovHeadDatum uT + Hask.pure $ govLHead'pkh govHead diff --git a/mlabs/src/Mlabs/NFT/Contract/Init.hs b/mlabs/src/Mlabs/NFT/Contract/Init.hs new file mode 100644 index 000000000..be96db2bc --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/Init.hs @@ -0,0 +1,147 @@ +module Mlabs.NFT.Contract.Init ( + createListHead, + getAppSymbol, + initApp, + uniqueTokenName, +) where + +import PlutusTx.Prelude hiding (mconcat, (<>)) +import Prelude (mconcat, (<>)) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Monoid (Last (..)) +import Data.Text (Text, pack) +import Text.Printf (printf) + +import Ledger (AssetClass, scriptCurrencySymbol) +import Ledger.Constraints qualified as Constraints +import Ledger.Typed.Scripts (validatorHash) +import Ledger.Value as Value (singleton) +import Plutus.Contract (Contract, mapError) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import Plutus.V1.Ledger.Value (TokenName (..), assetClass, assetClassValue) + +{- Drop-in replacement for +import Plutus.Contracts.Currency (CurrencyError, mintContract) +import Plutus.Contracts.Currency qualified as MC +till it will be fixed, see `Mlabs.Plutus.Contracts.Currency.mintContract` +for details -} +import Mlabs.Plutus.Contracts.Currency (CurrencyError, mintContract) +import Mlabs.Plutus.Contracts.Currency qualified as MC + +import Mlabs.Data.LinkedList (LList (..)) +import Mlabs.NFT.Contract.Aux (toDatum) +import Mlabs.NFT.Governance.Types (GovAct (..), GovDatum (..), GovLHead (..)) +import Mlabs.NFT.Governance.Validation (govMintPolicy, govScrAddress, govScript) +import Mlabs.NFT.Spooky (toSpooky, toSpookyAddress, toSpookyAssetClass, unSpookyAssetClass, unSpookyPubKeyHash) +import Mlabs.NFT.Types ( + DatumNft (HeadDatum), + GenericContract, + InitParams (..), + MintAct (Initialise), + NftAppInstance (NftAppInstance), + NftAppSymbol (NftAppSymbol), + NftListHead (NftListHead, head'appInstance', head'next'), + Pointer, + appInstance'UniqueToken, + ) +import Mlabs.NFT.Validation (asRedeemer, curSymbol, mintPolicy, txPolicy, txScrAddress) + +{- | The App Symbol is written to the Writter instance of the Contract to be + recovered for future opperations, and ease of use in Trace. +-} +type InitContract a = forall s. Contract (Last NftAppInstance) s Text a + +{- | + Initialise NFT marketplace, create HEAD of the list and unique token +-} +initApp :: InitParams -> InitContract () +initApp params = do + appInstance <- createListHead params + Contract.tell . Last . Just $ appInstance + Contract.logInfo @Hask.String $ printf "Finished Initialisation: App Instance: %s" (Hask.show appInstance) + +{- | Initialise the application at the address of the script by creating the + HEAD of the list, and coupling the one time token with the Head of the list. +-} +createListHead :: InitParams -> GenericContract NftAppInstance +createListHead InitParams {..} = do + uniqueToken <- generateUniqueToken + let govAddr = govScrAddress . toSpookyAssetClass $ uniqueToken + scrAddr = txScrAddress . toSpookyAssetClass $ uniqueToken + mintListHead $ NftAppInstance (toSpooky scrAddr) (toSpooky . toSpookyAssetClass $ uniqueToken) (toSpooky . toSpookyAddress $ govAddr) (toSpooky ip'admins) + where + -- Mint the Linked List Head and its associated token. + mintListHead :: NftAppInstance -> GenericContract NftAppInstance + mintListHead appInstance = do + let -- Unique Token + uniqueToken = appInstance'UniqueToken appInstance + uniqueTokenValue = assetClassValue (unSpookyAssetClass uniqueToken) 1 + emptyTokenName = TokenName PlutusTx.Prelude.emptyByteString + let -- Script Head Specific Information + headDatum :: DatumNft = nftHeadInit appInstance + headPolicy = mintPolicy appInstance + proofTokenValue = Value.singleton (scriptCurrencySymbol headPolicy) emptyTokenName 1 + initRedeemer = asRedeemer Initialise + let -- Gov App Head Specific information + govHeadDatum :: GovDatum = govHeadInit + govHeadPolicy = govMintPolicy appInstance + govScr = govScript uniqueToken + govProofTokenValue = Value.singleton (scriptCurrencySymbol govHeadPolicy) emptyTokenName 1 + govInitRedeemer = asRedeemer InitialiseGov + + -- NFT App Head + (lookups, tx) = + ( mconcat + [ Constraints.typedValidatorLookups (txPolicy uniqueToken) + , Constraints.mintingPolicy headPolicy + , Constraints.mintingPolicy govHeadPolicy + ] + , mconcat + [ Constraints.mustPayToTheScript (toBuiltinData headDatum) (proofTokenValue <> uniqueTokenValue) + , Constraints.mustPayToOtherScript (validatorHash govScr) (toDatum govHeadDatum) (govProofTokenValue <> uniqueTokenValue) + , Constraints.mustMintValueWithRedeemer initRedeemer proofTokenValue + , Constraints.mustMintValueWithRedeemer govInitRedeemer govProofTokenValue + ] + ) + void $ Contract.submitTxConstraintsWith lookups tx + Contract.logInfo @Hask.String $ printf "Forged Script Head & Governance Head for %s" (Hask.show appInstance) + return appInstance + + -- Contract that mints a unique token to be used in the minting of the head + generateUniqueToken :: GenericContract AssetClass + generateUniqueToken = do + self <- Contract.ownPaymentPubKeyHash + let tn = TokenName uniqueTokenName --PlutusTx.Prelude.emptyByteString + x <- + mapError + (pack . Hask.show @CurrencyError) + (mintContract self [(tn, 2)]) + return $ assetClass (MC.currencySymbol x) tn + + nftHeadInit :: NftAppInstance -> DatumNft + nftHeadInit appInst = + HeadDatum $ + NftListHead + { head'next' = toSpooky @(Maybe Pointer) Nothing + , head'appInstance' = toSpooky appInst + } + + govHeadInit = + GovDatum $ + HeadLList + { _head'info = GovLHead ip'feeRate (unSpookyPubKeyHash ip'feePkh) + , _head'next = Nothing + } + +-- | Given an App Instance return the NftAppSymbol for that app instance. +getAppSymbol :: NftAppInstance -> NftAppSymbol +getAppSymbol = NftAppSymbol . toSpooky . curSymbol + +{-# INLINEABLE uniqueTokenName #-} + +-- | Token Name with which Unique Tokens are parametrised. +uniqueTokenName :: BuiltinByteString +uniqueTokenName = "Unique App Token" diff --git a/mlabs/src/Mlabs/NFT/Contract/Mint.hs b/mlabs/src/Mlabs/NFT/Contract/Mint.hs new file mode 100644 index 000000000..82a39e11d --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/Mint.hs @@ -0,0 +1,192 @@ +{-# LANGUAGE UndecidableInstances #-} + +module Mlabs.NFT.Contract.Mint ( + mint, + getDatumsTxsOrdered, + mintParamsToInfo, +) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude (mconcat) +import Prelude qualified as Hask + +import Control.Monad (void) +import Data.Map qualified as Map +import Data.Monoid (Last (..)) +import Data.Text (Text) +import Text.Printf (printf) + +import Plutus.ChainIndex.Tx (txOutRefMapForAddr) +import Plutus.Contract (Contract) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) + +import Ledger (txOutValue) +import Ledger.Constraints qualified as Constraints +import Ledger.Typed.Scripts (validatorScript) +import Ledger.Value as Value (TokenName (..), assetClass, singleton) + +import Mlabs.NFT.Contract.Aux ( + getDatumsTxsOrdered, + getNftAppSymbol, + getNftHead, + getUId, + hashData, + ) +import Mlabs.NFT.Spooky (toSpooky, toSpookyAssetClass, unSpookyAddress, unSpookyCurrencySymbol) +import Mlabs.NFT.Types ( + AuctionState, + DatumNft (..), + GenericContract, + InformationNft (..), + InsertPoint (InsertPoint), + MintAct (Mint), + MintParams (..), + NftAppInstance, + NftId (NftId), + NftListHead (NftListHead), + NftListNode (..), + PointInfo (pi'CITx, pi'CITxO, pi'TOR, pi'data), + Pointer (Pointer), + UniqueToken, + UserAct (MintAct), + UserId, + UserWriter, + app'symbol, + appInstance'Address, + appInstance'UniqueToken, + getAppInstance, + getDatumValue, + info'id, + node'appInstance, + node'information, + ) +import Mlabs.NFT.Validation (asRedeemer, mintPolicy, txPolicy) + +-------------------------------------------------------------------------------- +-- MINT -- + +---- | Mints an NFT and sends it to the App Address. +mint :: forall s. UniqueToken -> MintParams -> Contract UserWriter s Text () +mint uT params = do + user <- getUId + head' <- getNftHead uT + case head' of + Nothing -> Contract.throwError @Text "Couldn't find head" + Just headX -> do + let appInstance = getAppInstance $ pi'data headX + newNode = createNewNode appInstance params user + nftPolicy = mintPolicy appInstance + (InsertPoint lNode rNode) <- findInsertPoint uT newNode + (lLk, lCx) <- updateNodePointer appInstance lNode newNode + (nLk, nCx) <- mintNode uT nftPolicy newNode rNode + let lookups = mconcat [lLk, nLk] + tx = mconcat [lCx, nCx] + void $ Contract.submitTxConstraintsWith lookups tx + Contract.tell . Last . Just . Left . info'id . node'information $ newNode + Contract.logInfo @Hask.String $ printf "mint successful!" + where + createNewNode :: NftAppInstance -> MintParams -> UserId -> NftListNode + createNewNode appInstance mp author = + NftListNode + { node'information' = toSpooky $ mintParamsToInfo mp author + , node'next' = toSpooky @(Maybe Pointer) Nothing + , node'appInstance' = toSpooky appInstance + } + + findInsertPoint :: UniqueToken -> NftListNode -> GenericContract (InsertPoint DatumNft) + findInsertPoint uT' node = do + list <- getDatumsTxsOrdered uT' + case list of + [] -> Contract.throwError "This Should never happen." + x : xs -> findPoint x xs + where + findPoint :: PointInfo DatumNft -> [PointInfo DatumNft] -> GenericContract (InsertPoint DatumNft) + findPoint x = \case + [] -> pure $ InsertPoint x Nothing + (y : ys) -> + case compare (pi'data y) (NodeDatum node) of + LT -> findPoint y ys + EQ -> Contract.throwError @Text "NFT already minted." + GT -> pure $ InsertPoint x (Just y) + + -- mintNode :: + -- UniqueToken -> + -- MintingPolicy -> + -- NftListNode -> + -- Maybe (PointInfo DatumNft) -> + -- GenericContract (Constraints.ScriptLookups Any, Constraints.TxConstraints i0 DatumNft) + mintNode uT' mintingP newNode nextNode = do + appSymbol <- getNftAppSymbol uT' + let newTokenValue = Value.singleton (unSpookyCurrencySymbol . app'symbol $ appSymbol) (TokenName . getDatumValue . NodeDatum $ newNode) 1 + aSymbol = app'symbol appSymbol + newTokenDatum = + NodeDatum $ + newNode + { node'next' = toSpooky (Pointer . toSpooky . toSpookyAssetClass . assetClass (unSpookyCurrencySymbol aSymbol) . TokenName . getDatumValue . pi'data <$> nextNode) + } + + mintRedeemer = asRedeemer . Mint . toSpooky . NftId . toSpooky . getDatumValue . NodeDatum $ newNode + + lookups = + mconcat + [ Constraints.typedValidatorLookups (txPolicy uT') + , Constraints.otherScript (validatorScript $ txPolicy uT') + , Constraints.mintingPolicy mintingP + ] + tx = + mconcat + [ Constraints.mustPayToTheScript (toBuiltinData newTokenDatum) newTokenValue + , Constraints.mustMintValueWithRedeemer mintRedeemer newTokenValue + ] + pure (lookups, tx) + + -- updateNodePointer :: + -- NftAppInstance -> + -- PointInfo DatumNft -> + -- NftListNode -> + -- GenericContract (Constraints.ScriptLookups Any, Constraints.TxConstraints i0 DatumNft) + updateNodePointer appInstance insertPoint newNode = do + appSymbol <- getNftAppSymbol (appInstance'UniqueToken appInstance) + let scriptAddr = appInstance'Address . node'appInstance $ newNode + token = + Ledger.txOutValue + . fst + $ (txOutRefMapForAddr (unSpookyAddress scriptAddr) (pi'CITx insertPoint) Map.! pi'TOR insertPoint) + newToken = assetClass (unSpookyCurrencySymbol . app'symbol $ appSymbol) (TokenName .getDatumValue . NodeDatum $ newNode) + newDatum = updatePointer (Pointer . toSpooky . toSpookyAssetClass $ newToken) + oref = pi'TOR insertPoint + redeemer = asRedeemer $ MintAct (toSpooky . NftId . toSpooky . getDatumValue . NodeDatum $ newNode) (toSpooky appSymbol) + oldDatum = pi'data insertPoint + + updatePointer :: Pointer -> DatumNft + updatePointer newPointer = + case oldDatum of + HeadDatum (NftListHead _ a) -> HeadDatum $ NftListHead (toSpooky $ Just newPointer) a + NodeDatum (NftListNode i _ a) -> NodeDatum $ NftListNode i (toSpooky $ Just newPointer) a + + lookups = + mconcat + [ Constraints.typedValidatorLookups (txPolicy uT) + , Constraints.otherScript (validatorScript $ txPolicy uT) + , Constraints.unspentOutputs $ Map.singleton (pi'TOR insertPoint) (pi'CITxO insertPoint) + ] + tx = + mconcat + [ Constraints.mustPayToTheScript (toBuiltinData newDatum) token + , Constraints.mustSpendScriptOutput oref redeemer + ] + pure (lookups, tx) + +mintParamsToInfo :: MintParams -> UserId -> InformationNft +mintParamsToInfo MintParams {..} author = + InformationNft + { info'id' = toSpooky $ nftIdInit mp'content + , info'share' = toSpooky mp'share + , info'price' = toSpooky mp'price + , info'owner' = toSpooky author + , info'author' = toSpooky author + , info'auctionState' = toSpooky @(Maybe AuctionState) Nothing + } + where + nftIdInit = NftId . toSpooky . hashData diff --git a/mlabs/src/Mlabs/NFT/Contract/OpenAuction.hs b/mlabs/src/Mlabs/NFT/Contract/OpenAuction.hs new file mode 100644 index 000000000..50b96d2d0 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/OpenAuction.hs @@ -0,0 +1,102 @@ +{-# LANGUAGE UndecidableInstances #-} +-- FIXME: Remove after uncommenting commented parts +{-# OPTIONS_GHC -Wno-unused-imports #-} + +module Mlabs.NFT.Contract.OpenAuction ( + openAuction, +) where + +import PlutusTx.Prelude hiding (mconcat, mempty, unless, (<>)) +import Prelude (mconcat) +import Prelude qualified as Hask + +import Control.Monad (unless, void, when) +import Data.Map qualified as Map +import Data.Monoid (Last (..)) +import Data.Text (Text) +import Text.Printf (printf) + +import Plutus.Contract (Contract) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import PlutusTx qualified + +import Ledger ( + Datum (..), + Redeemer (..), + ) + +import Ledger.Constraints qualified as Constraints +import Ledger.Typed.Scripts (validatorScript) +import Ledger.Value qualified as Value + +import Mlabs.NFT.Contract.Aux +import Mlabs.NFT.Spooky (toSpooky) +import Mlabs.NFT.Types +import Mlabs.NFT.Validation + +{- | + Attempts to start NFT auction, removes current price from NFT and starts auction. +-} +openAuction :: UniqueToken -> AuctionOpenParams -> Contract UserWriter s Text () +openAuction _ _ = error () + +-- openAuction uT (AuctionOpenParams nftId deadline minBid) = do +-- ownOrefTxOut <- getUserAddr >>= fstUtxoAt +-- symbol <- getNftAppSymbol uT +-- ownPkh <- Contract.ownPubKeyHash +-- PointInfo {..} <- findNft nftId uT +-- node <- case pi'data of +-- NodeDatum n -> Hask.pure n +-- _ -> Contract.throwError "NFT not found" + +-- let auctionState = info'auctionState . node'information $ node +-- isOwner = ownPkh == (getUserId . info'owner . node'information) node + +-- when (isJust auctionState) $ Contract.throwError "Can't open: auction is already in progress" +-- unless isOwner $ Contract.throwError "Only owner can start auction" + +-- userUtxos <- getUserUtxos +-- let nftDatum = NodeDatum $ updateDatum node +-- nftVal = Value.singleton (app'symbol symbol) (Value.TokenName . nftId'contentHash $ nftId) 1 +-- action = +-- OpenAuctionAct +-- { act'symbol' = toSpooky symbol +-- } +-- lookups = +-- mconcat +-- [ Constraints.unspentOutputs userUtxos +-- , Constraints.unspentOutputs $ Map.fromList [ownOrefTxOut] +-- , Constraints.unspentOutputs $ Map.fromList [(pi'TOR, pi'CITxO)] +-- , Constraints.typedValidatorLookups (txPolicy uT) +-- , Constraints.otherScript (validatorScript $ txPolicy uT) +-- ] +-- tx = +-- mconcat +-- [ Constraints.mustPayToTheScript (toBuiltinData nftDatum) nftVal +-- , Constraints.mustIncludeDatum (Datum . PlutusTx.toBuiltinData $ nftDatum) +-- , Constraints.mustSpendPubKeyOutput (fst ownOrefTxOut) +-- , Constraints.mustSpendScriptOutput +-- pi'TOR +-- (Redeemer . PlutusTx.toBuiltinData $ action) +-- ] +-- void $ Contract.submitTxConstraintsWith lookups tx +-- Contract.tell . Last . Just . Left $ nftId +-- void $ Contract.logInfo @Hask.String $ printf "Started auction for %s" $ Hask.show nftVal +-- where +-- newAuctionState = +-- AuctionState +-- { as'highestBid' = toSpooky @(Maybe Integer) Nothing +-- , as'deadline' = toSpooky deadline +-- , as'minBid' = toSpooky minBid +-- } + +-- updateDatum node = +-- node +-- { node'information' = +-- toSpooky $ +-- (node'information node) +-- { info'auctionState' = toSpooky $ Just newAuctionState +-- , info'price' = toSpooky @(Maybe Integer) Nothing +-- } +-- } diff --git a/mlabs/src/Mlabs/NFT/Contract/Query.hs b/mlabs/src/Mlabs/NFT/Contract/Query.hs new file mode 100644 index 000000000..ed226ecdd --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/Query.hs @@ -0,0 +1,115 @@ +module Mlabs.NFT.Contract.Query ( + queryCurrentOwnerLog, + queryCurrentPriceLog, + queryListNftsLog, + queryCurrentPrice, + queryCurrentOwner, + queryListNfts, + QueryContract, + queryContent, + queryContentLog, +) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude (String, show) + +import Data.Monoid (Last (..), mconcat) +import Data.Text (Text) +import GHC.Base (join) +import Plutus.Contract (Contract) +import Plutus.Contract qualified as Contract + +import Mlabs.NFT.Contract.Aux (getDatumsTxsOrdered, getNftDatum, getsNftDatum, hashData) +import Mlabs.NFT.Spooky (toSpooky) +import Mlabs.NFT.Types ( + Content, + DatumNft (..), + InformationNft, + NftId (NftId), + PointInfo (pi'data), + QueryResponse (..), + UniqueToken, + UserWriter, + info'owner, + info'price, + node'information, + ) + +-- | A contract used exclusively for query actions. +type QueryContract a = forall s. Contract UserWriter s Text a + +{- | Query the current price of a given NFTid. Writes it to the Writer instance + and also returns it, to be used in other contracts. +-} +queryCurrentPrice :: UniqueToken -> NftId -> QueryContract QueryResponse +queryCurrentPrice uT nftId = do + price <- wrap <$> getsNftDatum extractPrice nftId uT + Contract.tell (Last . Just . Right $ price) >> log price >> return price + where + wrap = QueryCurrentPrice . join + extractPrice = \case + HeadDatum _ -> Nothing + NodeDatum d -> info'price . node'information $ d + log price = Contract.logInfo @String $ queryCurrentPriceLog nftId price + +{- | Query the current owner of a given NFTid. Writes it to the Writer instance + and also returns it, to be used in other contracts. +-} +queryCurrentOwner :: UniqueToken -> NftId -> QueryContract QueryResponse +queryCurrentOwner uT nftId = do + owner <- wrap <$> getsNftDatum extractOwner nftId uT + Contract.tell (Last . Just . Right $ owner) >> log owner >> return owner + where + wrap = QueryCurrentOwner . join + extractOwner = \case + HeadDatum _ -> Nothing + NodeDatum d -> Just . info'owner . node'information $ d + log owner = Contract.logInfo @String $ queryCurrentOwnerLog nftId owner + +-- | Log of Current Price. Used in testing as well. +queryCurrentPriceLog :: NftId -> QueryResponse -> String +queryCurrentPriceLog nftId price = mconcat ["Current price of: ", show nftId, " is: ", show price] + +-- | Log msg of Current Owner. Used in testing as well. +queryCurrentOwnerLog :: NftId -> QueryResponse -> String +queryCurrentOwnerLog nftId owner = mconcat ["Current owner of: ", show nftId, " is: ", show owner] + +-- | Query the list of all NFTs in the app +queryListNfts :: UniqueToken -> QueryContract QueryResponse +queryListNfts uT = do + datums <- fmap pi'data <$> getDatumsTxsOrdered uT + let nodes = mapMaybe getNode datums + infos = node'information <$> nodes + Contract.tell $ wrap infos + Contract.logInfo @String $ queryListNftsLog infos + return $ QueryListNfts infos + where + getNode (NodeDatum node) = Just node + getNode _ = Nothing + + wrap = Last . Just . Right . QueryListNfts + +-- | Log of list of NFTs available in app. Used in testing as well. +queryListNftsLog :: [InformationNft] -> String +queryListNftsLog infos = mconcat ["Available NFTs: ", show infos] + +-- | Given an application instance and a `Content` returns the status of the NFT +queryContent :: UniqueToken -> Content -> QueryContract QueryResponse +queryContent uT content = do + let nftId = NftId . toSpooky . hashData $ content + datum <- getNftDatum nftId uT + status <- wrap $ getStatus datum + Contract.tell (Last . Just . Right $ status) + log status + return status + where + wrap = return . QueryContent + getStatus :: Maybe DatumNft -> Maybe InformationNft + getStatus = \case + Just (NodeDatum nftListNode) -> Just $ node'information nftListNode + _ -> Nothing + log status = Contract.logInfo @String $ queryContentLog content status + +-- | Log of status of a content. Used in testing as well. +queryContentLog :: Content -> QueryResponse -> String +queryContentLog content info = mconcat ["Content status of: ", show content, " is: ", show info] diff --git a/mlabs/src/Mlabs/NFT/Contract/SetPrice.hs b/mlabs/src/Mlabs/NFT/Contract/SetPrice.hs new file mode 100644 index 000000000..2ff047b24 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Contract/SetPrice.hs @@ -0,0 +1,95 @@ +{-# LANGUAGE UndecidableInstances #-} + +module Mlabs.NFT.Contract.SetPrice ( + setPrice, +) where + +import PlutusTx.Prelude hiding (mconcat) +import Prelude (mconcat) +import Prelude qualified as Hask + +import Control.Lens ((^.)) +import Control.Monad (void, when) +import Data.Map qualified as Map +import Data.Monoid (Last (..)) +import Data.Text (Text) + +import Plutus.Contract (Contract) +import Plutus.Contract qualified as Contract +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import PlutusTx qualified + +import Ledger (Redeemer (..), ciTxOutValue) +import Ledger.Constraints qualified as Constraints +import Ledger.Typed.Scripts (validatorScript) + +import Mlabs.NFT.Contract.Aux ( + findNft, + fstUtxoAt, + getNftAppSymbol, + getUserAddr, + ) +import Mlabs.NFT.Spooky (toSpooky, toSpookyPaymentPubKeyHash) +import Mlabs.NFT.Types ( + DatumNft (NodeDatum), + InformationNft (info'price'), + NftListNode (node'information'), + PointInfo (PointInfo, pi'CITx, pi'CITxO, pi'TOR, pi'data), + SetPriceParams (..), + UniqueToken, + UserAct (SetPriceAct), + UserWriter, + getUserId, + info'auctionState, + info'owner, + node'information, + ) +import Mlabs.NFT.Validation (txPolicy) + +{- | + Attempts to set price of NFT, checks if price is being set by the owner + and that NFT is not on an auction. +-} +setPrice :: UniqueToken -> SetPriceParams -> Contract UserWriter s Text () +setPrice ut SetPriceParams {..} = do + aSymbol <- getNftAppSymbol ut + when negativePrice $ Contract.throwError "New price can not be negative" + ownOrefTxOut <- getUserAddr >>= fstUtxoAt + ownPkh <- Contract.ownPaymentPubKeyHash + PointInfo {..} <- findNft sp'nftId ut + oldNode <- case pi'data of + NodeDatum n -> Hask.pure n + _ -> Contract.throwError "NFT not found" + when (getUserId ((info'owner . node'information) oldNode) /= toSpookyPaymentPubKeyHash ownPkh) $ + Contract.throwError "Only owner can set price" + when (isJust . info'auctionState . node'information $ oldNode) $ + Contract.throwError "Can't set price auction is already in progress" + + let nftDatum = NodeDatum $ updateDatum oldNode + nftVal = pi'CITxO ^. ciTxOutValue + action = SetPriceAct (toSpooky sp'price) (toSpooky aSymbol) + lookups = + mconcat + [ Constraints.unspentOutputs $ Map.fromList [ownOrefTxOut] + , Constraints.unspentOutputs $ Map.fromList [(pi'TOR, pi'CITxO)] + , Constraints.typedValidatorLookups (txPolicy ut) + , Constraints.otherScript (validatorScript . txPolicy $ ut) + ] + tx = + mconcat + [ Constraints.mustPayToTheScript (toBuiltinData nftDatum) nftVal + , Constraints.mustSpendPubKeyOutput (fst ownOrefTxOut) + , Constraints.mustSpendScriptOutput + pi'TOR + (Redeemer . PlutusTx.toBuiltinData $ action) + ] + + void $ Contract.submitTxConstraintsWith lookups tx + Contract.tell . Last . Just . Left $ sp'nftId + Contract.logInfo @Hask.String "set-price successful!" + where + updateDatum node = node {node'information' = toSpooky ((node'information node) {info'price' = toSpooky sp'price})} + + negativePrice = case sp'price of + Nothing -> False + Just v -> v < 0 diff --git a/mlabs/src/Mlabs/NFT/Governance.hs b/mlabs/src/Mlabs/NFT/Governance.hs new file mode 100644 index 000000000..dfdea5d63 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Governance.hs @@ -0,0 +1,6 @@ +{- Module used for re-exporting sub-modules -} + +module Mlabs.NFT.Governance (module X) where + +import Mlabs.NFT.Governance.Types as X +import Mlabs.NFT.Governance.Validation as X diff --git a/mlabs/src/Mlabs/NFT/Governance/Types.hs b/mlabs/src/Mlabs/NFT/Governance/Types.hs new file mode 100644 index 000000000..d75696e87 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Governance/Types.hs @@ -0,0 +1,69 @@ +module Mlabs.NFT.Governance.Types ( + GovAct (..), + GovLHead (..), + GovLNode (..), + GovLList, + GovDatum (..), + LList (..), +) where + +import Mlabs.Data.LinkedList (LList (..)) +import Mlabs.NFT.Types (UserId) +import Prelude qualified as Hask + +import PlutusTx qualified + +import Data.Aeson (FromJSON, ToJSON) +import GHC.Generics (Generic) +import Plutus.V1.Ledger.Crypto (PubKeyHash) +import PlutusTx.Prelude + +-- | Datum for utxo containing GovLList Head token. +data GovLHead = GovLHead + { govLHead'feeRate :: Rational + , govLHead'pkh :: PubKeyHash + } + deriving stock (Hask.Show, Generic, Hask.Eq, Hask.Ord) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.unstableMakeIsData ''GovLHead +PlutusTx.makeLift ''GovLHead + +instance Eq GovLHead where + {-# INLINEABLE (==) #-} + (GovLHead a b) == (GovLHead a' b') = a == a' && b == b' + +-- | Datum for utxo containing GovLList Head token. +data GovLNode = GovLNode + deriving stock (Hask.Show, Generic, Hask.Eq, Hask.Ord) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.unstableMakeIsData ''GovLNode +PlutusTx.makeLift ''GovLNode + +instance Eq GovLNode where + {-# INLINEABLE (==) #-} + _ == _ = True + +type GovLList = LList UserId GovLHead GovLNode + +newtype GovDatum = GovDatum {gov'list :: GovLList} + deriving stock (Hask.Show, Generic, Hask.Eq, Hask.Ord) + deriving newtype (Eq, Ord) + deriving anyclass (ToJSON, FromJSON) +PlutusTx.unstableMakeIsData ''GovDatum +PlutusTx.makeLift ''GovDatum + +data GovAct + = -- | Mint Governance Tokens + MintGov -- Gov Token is added / update on list, and as many xGov tokens are created and relelased. + | -- | Use as Proof + Proof -- Token is used as proof and must be returned unchanged to the application + | -- | Use as Proof and Burn + ProofAndBurn -- Token is used as proof and must be burned in totality. + | -- | Initialises the Governance List at the given location + InitialiseGov + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) +PlutusTx.unstableMakeIsData ''GovAct +PlutusTx.makeLift ''GovAct diff --git a/mlabs/src/Mlabs/NFT/Governance/Validation.hs b/mlabs/src/Mlabs/NFT/Governance/Validation.hs new file mode 100644 index 000000000..43ae94ae1 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Governance/Validation.hs @@ -0,0 +1,96 @@ +module Mlabs.NFT.Governance.Validation ( + govScript, + govMintPolicy, + govScrAddress, + GovManage, +) where + +--import Prelude qualified as Hask + +import Ledger ( + Address, + MintingPolicy, + ScriptContext, + mkMintingPolicyScript, + ) +import Ledger.Typed.Scripts ( + TypedValidator, + ValidatorTypes (..), + mkTypedValidator, + validatorAddress, + wrapMintingPolicy, + wrapValidator, + ) + +import Mlabs.NFT.Governance.Types (GovAct (..), GovDatum) +import Mlabs.NFT.Types (NftAppInstance, UniqueToken) +import PlutusTx qualified +import PlutusTx.Prelude (Bool (True), ($), (.)) + +data GovManage +instance ValidatorTypes GovManage where + type DatumType GovManage = GovDatum + type RedeemerType GovManage = GovAct + +{-# INLINEABLE mkGovMintPolicy #-} + +-- | Minting policy for GOV and xGOV tokens. +mkGovMintPolicy :: NftAppInstance -> GovAct -> ScriptContext -> Bool +mkGovMintPolicy _ act _ = + case act of + InitialiseGov -> + True + MintGov -> + True -- makes sure that 1:1 Gov/xGov tokens are minted with the amount of + -- lovelace paid at the Treasury address. Makes sure that the Gov is + -- inserted to the linked list (correctly). + Proof -> + True -- does nothing (i.e. makes sure nothing gets minted) + ProofAndBurn -> + True -- makes sure that Gov/xGov is removed and the Gov linked list is + -- updated accordingly. + +{-# INLINEABLE govMintPolicy #-} + +-- | Gov Minting Policy +govMintPolicy :: NftAppInstance -> MintingPolicy +govMintPolicy x = + mkMintingPolicyScript $ + $$(PlutusTx.compile [||wrapMintingPolicy . mkGovMintPolicy||]) + `PlutusTx.applyCode` PlutusTx.liftCode x + +{-# INLINEABLE mkGovScript #-} + +-- | Minting policy for GOV and xGOV tokens. +mkGovScript :: UniqueToken -> GovDatum -> GovAct -> ScriptContext -> Bool +mkGovScript _ _ act _ = + case act of + InitialiseGov -> + True + MintGov -> + True -- makes sure that the correct fees are paid, and the correct amount + -- of Gov/xGov is minted. Also makes sure that the Gov/xGov are sent + -- to the correct addresses, and that the Gov list is not altered + -- maliciously. + Proof -> + True -- makes sure that the token is used as proof and returned to the Gov + -- Address, with nothing being altered. + ProofAndBurn -> + True -- makes sure, that the corresponding Gov to the xGov is removed from + -- the list. The user can also claim their locked lovelace back (take + -- their stake out of the app). + +{-# INLINEABLE govScript #-} +govScript :: UniqueToken -> TypedValidator GovManage +govScript x = + mkTypedValidator @GovManage + ($$(PlutusTx.compile [||mkGovScript||]) `PlutusTx.applyCode` PlutusTx.liftCode x) + $$(PlutusTx.compile [||wrap||]) + where + wrap = wrapValidator @GovDatum @GovAct + +{-# INLINEABLE govScrAddress #-} + +-- | Address of Gov Script Logic. +govScrAddress :: UniqueToken -> Ledger.Address +govScrAddress = validatorAddress . govScript diff --git a/mlabs/src/Mlabs/NFT/PAB/MarketplaceContract.hs b/mlabs/src/Mlabs/NFT/PAB/MarketplaceContract.hs new file mode 100644 index 000000000..2f9d38be4 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/PAB/MarketplaceContract.hs @@ -0,0 +1,59 @@ +module Mlabs.NFT.PAB.MarketplaceContract ( + MarketplaceContracts (..), +) where + +import PlutusTx.Prelude +import Prelude qualified as Hask + +import Data.Aeson ( + FromJSON, + ToJSON, + ) +import Data.OpenApi.Schema qualified as OpenApi + +import GHC.Generics (Generic) + +import Prettyprinter (Pretty (..), viaShow) + +import Plutus.PAB.Effects.Contract.Builtin (HasDefinitions (..), SomeBuiltin (..)) +import Plutus.PAB.Effects.Contract.Builtin qualified as Builtin + +import Mlabs.NFT.Api qualified as Contract.NFT +import Mlabs.NFT.Contract.Init (uniqueTokenName) +import Mlabs.NFT.Spooky (toSpookyAssetClass) +import Mlabs.NFT.Types (UniqueToken) + +import Plutus.Contracts.Currency () +import Plutus.V1.Ledger.Value (CurrencySymbol (..), TokenName (..), assetClass) + +{- | Contracts available through PAB. + For concrete endpoints see `getContract` +-} +data MarketplaceContracts + = -- | Contract for initialising NFT marketplace. + NftAdminContract + | -- | Contracts for NFT marketplace user - contracts for + -- buying/selling NFT, auctions, and query. + UserContract UniqueToken + deriving stock (Hask.Eq, Hask.Ord, Hask.Show, Generic) + deriving anyclass (FromJSON, ToJSON, OpenApi.ToSchema) + +instance Pretty MarketplaceContracts where + pretty = viaShow + +-- todo: fix put correct currencySymbol. +instance HasDefinitions MarketplaceContracts where + getDefinitions = + [ NftAdminContract + , UserContract uT + ] + where + uT = toSpookyAssetClass $ assetClass (CurrencySymbol "ff") (TokenName uniqueTokenName) + + getContract = \case + NftAdminContract -> SomeBuiltin Contract.NFT.adminEndpoints + UserContract uT -> SomeBuiltin $ Contract.NFT.nftMarketUserEndpoints uT + + getSchema = \case + NftAdminContract -> Builtin.endpointsToSchemas @Contract.NFT.NFTAppSchema + UserContract _ -> Builtin.endpointsToSchemas @Contract.NFT.NFTAppSchema diff --git a/mlabs/src/Mlabs/NFT/PAB/Run.hs b/mlabs/src/Mlabs/NFT/PAB/Run.hs new file mode 100644 index 000000000..5f4f75fbf --- /dev/null +++ b/mlabs/src/Mlabs/NFT/PAB/Run.hs @@ -0,0 +1,14 @@ +module Mlabs.NFT.PAB.Run ( + runNftMarketplace, +) where + +import Plutus.PAB.Effects.Contract.Builtin qualified as Builtin +import Plutus.PAB.Run (runWith) +import Prelude + +import Mlabs.NFT.PAB.MarketplaceContract (MarketplaceContracts) + +-- | Start PAB for NFT contract +runNftMarketplace :: IO () +runNftMarketplace = do + runWith (Builtin.handleBuiltin @MarketplaceContracts) diff --git a/mlabs/src/Mlabs/NFT/PAB/Simulator.hs b/mlabs/src/Mlabs/NFT/PAB/Simulator.hs new file mode 100644 index 000000000..b283af294 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/PAB/Simulator.hs @@ -0,0 +1,36 @@ +module Mlabs.NFT.PAB.Simulator ( + handlers, + runSimulator, +) where + +-- import PlutusTx.Prelude +import Prelude + +import Control.Monad (void) +import Control.Monad.Freer +import Control.Monad.IO.Class (MonadIO (..)) +import Data.Default (Default (def)) + +import Plutus.PAB.Effects.Contract.Builtin (Builtin, BuiltinHandler (..)) +import Plutus.PAB.Effects.Contract.Builtin qualified as Builtin +import Plutus.PAB.Simulator (SimulatorEffectHandlers, logString) +import Plutus.PAB.Simulator qualified as Simulator +import Plutus.PAB.Webserver.Server qualified as PAB.Server + +import Mlabs.NFT.PAB.MarketplaceContract (MarketplaceContracts) + +-- | Start PAB simulator for NFT contracts +runSimulator :: IO () +runSimulator = void $ + Simulator.runSimulationWith handlers $ do + logString @(Builtin MarketplaceContracts) + "Starting NFT marketplace simulated PAB webserver. Press enter to exit." + shutdown <- PAB.Server.startServerDebug + _ <- liftIO getLine + shutdown + +-- | Simulator handlers for NFT contracts +handlers :: SimulatorEffectHandlers (Builtin MarketplaceContracts) +handlers = + Simulator.mkSimulatorHandlers def def $ + interpret (contractHandler Builtin.handleBuiltin) diff --git a/mlabs/src/Mlabs/NFT/Spooky.hs b/mlabs/src/Mlabs/NFT/Spooky.hs new file mode 100644 index 000000000..b5452eb3e --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Spooky.hs @@ -0,0 +1,668 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +module Mlabs.NFT.Spooky ( + ValidatorHash (..), + PubKeyHash (..), + getPubKeyHash, + toSpookyPubKeyHash, + unSpookyPubKeyHash, + PaymentPubKeyHash (..), + unPaymentPubKeyHash, + toSpookyPaymentPubKeyHash, + unSpookyPaymentPubKeyHash, + DatumHash (..), + getDatumHash, + CurrencySymbol (..), + toSpookyCurrencySymbol, + unSpookyCurrencySymbol, + TokenName (..), + toSpookyTokenName, + unSpookyTokenName, + unTokenName, + Value (..), + unSpookyValue, + flattenValue, + singleton, + valueOf, + lovelaceValueOf, + symbols, + adaSymbol, + adaToken, + AssetClass (..), + toSpookyAssetClass, + unSpookyAssetClass, + unAssetClass, + assetClass, + assetClassValue, + assetClassValueOf, + Credential (..), + StakingCredential (..), + Address (..), + toSpookyAddress, + unSpookyAddress, + mkTypedValidator, + TxId (..), + getTxId, + TxOutRef (..), + txOutRefId, + txOutRefIdx, + ScriptPurpose (..), + TxOut (..), + txOutAddress, + txOutValue, + txOutDatumHash, + TxInInfo (..), + txInInfoOutRef, + txInInfoResolved, + TxInfo (..), + txInfoData, + txInfoSignatories, + txInfoOutputs, + txInfoInputs, + txInfoMint, + valuePaidTo, + pubKeyOutputsAt, + findDatum, + ScriptContext (..), + scriptContextTxInfo, + scriptContextPurpose, + ownCurrencySymbol, + Spooky, + toSpooky, + unSpooky, +) where + +import PlutusTx qualified +import PlutusTx.Prelude +import Prelude qualified as Hask + +import Data.Kind (Type) +import GHC.Generics (Generic) + +import Control.Monad (guard) +import Data.OpenApi.Schema qualified as OpenApi +import Ledger ( + Datum, + POSIXTimeRange, + ) +import Ledger qualified +import Ledger.Crypto qualified as Crypto +import Ledger.Scripts qualified as Scripts +import Ledger.Typed.Scripts.Validators (DatumType, RedeemerType, TypedValidator, WrappedValidatorType) +import Ledger.Typed.Scripts.Validators qualified as Validators +import Playground.Contract (FromJSON, ToJSON, ToSchema) +import Plutus.V1.Ledger.Api (DCert) +import Plutus.V1.Ledger.Credential qualified as Credential +import Plutus.V1.Ledger.Value qualified as Value +import PlutusTx.AssocMap qualified as Map +import PlutusTx.Spooky +import PlutusTx.These (These (..)) +import Schema (ToSchema (toSchema)) +import Unsafe.Coerce (unsafeCoerce) + +instance ToSchema BuiltinData where + toSchema = toSchema @Hask.String + +newtype ValidatorHash = ValidatorHash {getValidatorHash' :: Spooky BuiltinByteString} + deriving stock (Generic, Hask.Show) + deriving newtype (Hask.Eq, Hask.Ord, Eq, PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.makeLift ''ValidatorHash + +newtype PubKeyHash = PubKeyHash {getPubKeyHash' :: Spooky BuiltinByteString} + deriving stock (Generic, Hask.Show) + deriving newtype (Hask.Eq, Hask.Ord, Eq, PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.makeLift ''PubKeyHash + +instance Ord PubKeyHash where + {-# INLINEABLE compare #-} + compare h h' = compare (getPubKeyHash h) (getPubKeyHash h') + +{-# INLINEABLE getPubKeyHash #-} +getPubKeyHash :: PubKeyHash -> BuiltinByteString +getPubKeyHash = unSpooky . getPubKeyHash' + +{-# INLINEABLE toSpookyPubKeyHash #-} +toSpookyPubKeyHash :: Crypto.PubKeyHash -> PubKeyHash +toSpookyPubKeyHash (Crypto.PubKeyHash hash) = PubKeyHash . toSpooky $ hash + +{-# INLINEABLE unSpookyPubKeyHash #-} +unSpookyPubKeyHash :: PubKeyHash -> Crypto.PubKeyHash +unSpookyPubKeyHash (PubKeyHash hash) = Crypto.PubKeyHash . unSpooky $ hash + +newtype PaymentPubKeyHash = PaymentPubKeyHash {unPaymentPubKeyHash' :: Spooky PubKeyHash} + deriving stock (Generic, Hask.Show) + deriving newtype (Hask.Eq, Hask.Ord, Eq, PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.makeLift ''PaymentPubKeyHash + +instance Ord PaymentPubKeyHash where + {-# INLINEABLE compare #-} + compare h h' = compare (unPaymentPubKeyHash h) (unPaymentPubKeyHash h') + +{-# INLINEABLE unPaymentPubKeyHash #-} +unPaymentPubKeyHash :: PaymentPubKeyHash -> PubKeyHash +unPaymentPubKeyHash = unSpooky . unPaymentPubKeyHash' + +{-# INLINEABLE toSpookyPaymentPubKeyHash #-} +toSpookyPaymentPubKeyHash :: Ledger.PaymentPubKeyHash -> PaymentPubKeyHash +toSpookyPaymentPubKeyHash (Ledger.PaymentPubKeyHash hash) = PaymentPubKeyHash . toSpooky . toSpookyPubKeyHash $ hash + +{-# INLINEABLE unSpookyPaymentPubKeyHash #-} +unSpookyPaymentPubKeyHash :: PaymentPubKeyHash -> Ledger.PaymentPubKeyHash +unSpookyPaymentPubKeyHash (PaymentPubKeyHash hash) = Ledger.PaymentPubKeyHash . unSpookyPubKeyHash . unSpooky $ hash + +newtype DatumHash = DatumHash {getDatumHash' :: Spooky BuiltinByteString} + deriving stock (Generic) + deriving newtype (Hask.Eq, Hask.Ord, Eq, PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) + +{-# INLINEABLE getDatumHash #-} +getDatumHash :: DatumHash -> BuiltinByteString +getDatumHash = unSpooky . getDatumHash' + +newtype CurrencySymbol = CurrencySymbol {unCurrencySymbol' :: Spooky BuiltinByteString} + deriving stock (Generic, Hask.Show) + deriving newtype (Hask.Eq, Hask.Ord, Eq, PlutusTx.UnsafeFromData, PlutusTx.FromData, PlutusTx.ToData) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.makeLift ''CurrencySymbol + +instance Ord CurrencySymbol where + {-# INLINEABLE compare #-} + compare cs cs' = compare (unCurrencySymbol cs) (unCurrencySymbol cs') + +{-# INLINEABLE unCurrencySymbol #-} +unCurrencySymbol :: CurrencySymbol -> BuiltinByteString +unCurrencySymbol = unSpooky . unCurrencySymbol' + +{-# INLINEABLE toSpookyCurrencySymbol #-} +toSpookyCurrencySymbol :: Ledger.CurrencySymbol -> CurrencySymbol +toSpookyCurrencySymbol (Value.CurrencySymbol cs) = CurrencySymbol . toSpooky $ cs + +{-# INLINEABLE unSpookyCurrencySymbol #-} +unSpookyCurrencySymbol :: CurrencySymbol -> Ledger.CurrencySymbol +unSpookyCurrencySymbol (CurrencySymbol cs) = Value.CurrencySymbol . unSpooky $ cs + +newtype TokenName = TokenName {unTokenName' :: Spooky BuiltinByteString} + deriving stock (Generic, Hask.Show) + deriving newtype (Hask.Eq, Hask.Ord, Eq, PlutusTx.UnsafeFromData, PlutusTx.FromData, PlutusTx.ToData) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.makeLift ''TokenName + +instance Ord TokenName where + {-# INLINEABLE compare #-} + compare tn tn' = compare (unTokenName tn) (unTokenName tn') + +{-# INLINEABLE unTokenName #-} +unTokenName :: TokenName -> BuiltinByteString +unTokenName = unSpooky . unTokenName' + +{-# INLINEABLE toSpookyTokenName #-} +toSpookyTokenName :: Ledger.TokenName -> TokenName +toSpookyTokenName (Value.TokenName tn) = TokenName . toSpooky $ tn + +{-# INLINEABLE unSpookyTokenName #-} +unSpookyTokenName :: TokenName -> Ledger.TokenName +unSpookyTokenName (TokenName tn) = Value.TokenName . unSpooky $ tn + +newtype AssetClass = AssetClass {unAssetClass' :: Spooky (CurrencySymbol, TokenName)} + deriving stock (Generic, Hask.Show) + deriving newtype (Hask.Eq, Hask.Ord, Eq, PlutusTx.UnsafeFromData, PlutusTx.FromData, PlutusTx.ToData) + deriving anyclass (ToJSON, FromJSON, ToSchema, OpenApi.ToSchema) +PlutusTx.makeLift ''AssetClass + +instance Ord AssetClass where + {-# INLINEABLE compare #-} + compare ac ac' = compare (unAssetClass ac) (unAssetClass ac') + +{-# INLINEABLE unAssetClass #-} +unAssetClass :: AssetClass -> (CurrencySymbol, TokenName) +unAssetClass = unSpooky . unAssetClass' + +{-# INLINEABLE toSpookyAssetClass #-} +toSpookyAssetClass :: Ledger.AssetClass -> AssetClass +toSpookyAssetClass ac = + let (c, t) = Value.unAssetClass ac + in assetClass (toSpookyCurrencySymbol c) (toSpookyTokenName t) + +{-# INLINEABLE unSpookyAssetClass #-} +unSpookyAssetClass :: AssetClass -> Ledger.AssetClass +unSpookyAssetClass ac = + let (c, t) = unAssetClass ac + in Value.assetClass (unSpookyCurrencySymbol c) (unSpookyTokenName t) + +{-# INLINEABLE assetClass #-} +assetClass :: CurrencySymbol -> TokenName -> AssetClass +assetClass s t = AssetClass $ toSpooky (s, t) + +newtype Value = Value {getValue' :: Map.Map CurrencySymbol (Map.Map TokenName Integer)} + deriving stock (Generic) + deriving newtype (PlutusTx.UnsafeFromData, PlutusTx.FromData, PlutusTx.ToData) + deriving anyclass (ToJSON, FromJSON) +PlutusTx.makeLift ''Value + +instance Hask.Semigroup Value where + (<>) = unionWith (+) + +instance Semigroup Value where + {-# INLINEABLE (<>) #-} + (<>) = unionWith (+) + +instance Hask.Monoid Value where + mempty = Value Map.empty + +instance Monoid Value where + {-# INLINEABLE mempty #-} + mempty = Value Map.empty + +instance Hask.Eq Value where + (==) = eq + +instance Eq Value where + {-# INLINEABLE (==) #-} + (==) = eq + +{-# INLINEABLE unSpookyValue #-} +unSpookyValue :: Value -> Value.Value +unSpookyValue = + mconcat + . fmap (\(cs, tn, v) -> Value.singleton (unSpookyCurrencySymbol cs) (unSpookyTokenName tn) v) + . flattenValue + +{-# INLINEABLE checkPred #-} +checkPred :: (These Integer Integer -> Bool) -> Value -> Value -> Bool +checkPred f l r = + let inner :: Map.Map TokenName (These Integer Integer) -> Bool + inner = Map.all f + in Map.all inner (unionVal l r) + +{-# INLINEABLE checkBinRel #-} + +{- | Check whether a binary relation holds for value pairs of two 'Value' maps, + supplying 0 where a key is only present in one of them. +-} +checkBinRel :: (Integer -> Integer -> Bool) -> Value -> Value -> Bool +checkBinRel f l r = + let unThese k' = case k' of + This a -> f a 0 + That b -> f 0 b + These a b -> f a b + in checkPred unThese l r + +{-# INLINEABLE eq #-} + +-- | Check whether one 'Value' is equal to another. See 'Value' for an explanation of how operations on 'Value's work. +eq :: Value -> Value -> Bool +-- If both are zero then checkBinRel will be vacuously true, but this is fine. +eq = checkBinRel (==) + +{-# INLINEABLE unionVal #-} + +-- | Combine two 'Value' maps +unionVal :: Value -> Value -> Map.Map CurrencySymbol (Map.Map TokenName (These Integer Integer)) +unionVal (Value l) (Value r) = + let combined = Map.union l r + unThese k = case k of + This a -> This <$> a + That b -> That <$> b + These a b -> Map.union a b + in unThese <$> combined + +{-# INLINEABLE unionWith #-} +unionWith :: (Integer -> Integer -> Integer) -> Value -> Value -> Value +unionWith f ls rs = + let combined = unionVal ls rs + unThese k' = case k' of + This a -> f a 0 + That b -> f 0 b + These a b -> f a b + in Value (fmap (fmap unThese) combined) + +{-# INLINEABLE flattenValue #-} +flattenValue :: Value -> [(CurrencySymbol, TokenName, Integer)] +flattenValue (Value v) = do + (cs, m) <- Map.toList v + (tn, a) <- Map.toList m + guard $ a /= 0 + return (cs, tn, a) + +{-# INLINEABLE singleton #-} + +-- | Make a 'Value' containing only the given quantity of the given currency. +singleton :: CurrencySymbol -> TokenName -> Integer -> Value +singleton c tn i = Value (Map.singleton c (Map.singleton tn i)) + +{-# INLINEABLE valueOf #-} + +-- | Get the quantity of the given currency in the 'Value'. +valueOf :: Value -> CurrencySymbol -> TokenName -> Integer +valueOf (Value mp) cur tn = + case Map.lookup cur mp of + Nothing -> 0 :: Integer + Just i -> fromMaybe 0 (Map.lookup tn i) + +{-# INLINEABLE lovelaceValueOf #-} +lovelaceValueOf :: Integer -> Value +lovelaceValueOf = singleton adaSymbol adaToken + +{-# INLINEABLE symbols #-} + +-- | The list of 'CurrencySymbol's of a 'Value'. +symbols :: Value -> [CurrencySymbol] +symbols (Value mp) = Map.keys mp + +{-# INLINEABLE assetClassValue #-} + +-- | A 'Value' containing the given amount of the asset class. +assetClassValue :: AssetClass -> Integer -> Value +assetClassValue ac i = + let (c, t) = unAssetClass ac + in singleton c t i + +{-# INLINEABLE assetClassValueOf #-} + +-- | Get the quantity of the given 'AssetClass' class in the 'Value'. +assetClassValueOf :: Value -> AssetClass -> Integer +assetClassValueOf v ac = + let (c, t) = unAssetClass ac + in valueOf v c t + +{-# INLINEABLE adaSymbol #-} +adaSymbol :: CurrencySymbol +adaSymbol = CurrencySymbol . toSpooky @BuiltinByteString $ "" + +{-# INLINEABLE adaToken #-} +adaToken :: TokenName +adaToken = TokenName . toSpooky @BuiltinByteString $ "" + +data Credential + = PubKeyCredential (Spooky PubKeyHash) + | ScriptCredential (Spooky ValidatorHash) + deriving stock (Generic, Hask.Show, Hask.Eq) +PlutusTx.unstableMakeIsData ''Credential +PlutusTx.makeLift ''Credential + +instance Eq Credential where + {-# INLINEABLE (==) #-} + PubKeyCredential pkh == PubKeyCredential pkh' = pkh == pkh' + ScriptCredential vh == ScriptCredential vh' = vh == vh' + _ == _ = False + +{-# INLINEABLE unSpookyCredential #-} +unSpookyCredential :: Credential -> Credential.Credential +unSpookyCredential (PubKeyCredential pkh) = Credential.PubKeyCredential (unSpooky pkh) +unSpookyCredential (ScriptCredential hash) = Credential.ScriptCredential (unSpooky hash) + +{-# INLINEABLE toSpookyCredential #-} +toSpookyCredential :: Credential.Credential -> Credential +toSpookyCredential (Credential.PubKeyCredential pkh) = PubKeyCredential (toSpooky pkh) +toSpookyCredential (Credential.ScriptCredential hash) = ScriptCredential (toSpooky hash) + +data StakingCredential + = StakingHash (Spooky Credential) + | StakingPtr (Spooky Integer) (Spooky Integer) (Spooky Integer) + deriving stock (Generic, Hask.Show, Hask.Eq) +PlutusTx.unstableMakeIsData ''StakingCredential +PlutusTx.makeLift ''StakingCredential + +instance Eq StakingCredential where + {-# INLINEABLE (==) #-} + StakingHash c == StakingHash c' = c == c' + StakingPtr a b c == StakingPtr a' b' c' = + a == a' + && b == b' + && c == c' + _ == _ = False + +{-# INLINEABLE unSpookyStakingCredential #-} +unSpookyStakingCredential :: StakingCredential -> Credential.StakingCredential +unSpookyStakingCredential (StakingHash hash) = Credential.StakingHash (unSpookyCredential . unSpooky $ hash) +unSpookyStakingCredential (StakingPtr a b c) = Credential.StakingPtr (unSpooky a) (unSpooky b) (unSpooky c) + +{-# INLINEABLE toSpookyStakingCredential #-} +toSpookyStakingCredential :: Credential.StakingCredential -> StakingCredential +toSpookyStakingCredential (Credential.StakingHash pkh) = StakingHash (toSpooky . toSpookyCredential $ pkh) +toSpookyStakingCredential (Credential.StakingPtr a b c) = StakingPtr (toSpooky a) (toSpooky b) (toSpooky c) + +data Address = Address + { addressCredential' :: Spooky Credential + , addressStakingCredential' :: Spooky (Maybe StakingCredential) + } + deriving stock (Generic, Hask.Show, Hask.Eq) +PlutusTx.unstableMakeIsData ''Address +PlutusTx.makeLift ''Address + +instance Eq Address where + {-# INLINEABLE (==) #-} + Address c s == Address c' s' = + c == c' + && s == s' + +{-# INLINEABLE unSpookyAddress #-} +unSpookyAddress :: Address -> Ledger.Address +unSpookyAddress (Address cred sCred) = + Ledger.Address (unSpookyCredential . unSpooky $ cred) (fmap unSpookyStakingCredential . unSpooky $ sCred) + +{-# INLINEABLE toSpookyAddress #-} +toSpookyAddress :: Ledger.Address -> Address +toSpookyAddress (Ledger.Address cred sCred) = + Address (toSpooky . toSpookyCredential $ cred) (toSpooky . fmap toSpookyStakingCredential $ sCred) + +newtype TxId = TxId {getTxId' :: Spooky BuiltinByteString} + deriving stock (Generic, Hask.Show) + deriving newtype (Hask.Eq, Hask.Ord, Eq, PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.makeLift ''TxId + +{-# INLINEABLE getTxId #-} +getTxId :: TxId -> BuiltinByteString +getTxId = unSpooky . getTxId' + +data TxOutRef = TxOutRef + { txOutRefId' :: Spooky TxId + , txOutRefIdx' :: Spooky Integer + } + deriving stock (Generic, Hask.Show, Hask.Eq, Hask.Ord) +PlutusTx.unstableMakeIsData ''TxOutRef + +instance Eq TxOutRef where + {-# INLINEABLE (==) #-} + TxOutRef a b == TxOutRef a' b' = + a == a' + && b == b' + +{-# INLINEABLE txOutRefId #-} +txOutRefId :: TxOutRef -> TxId +txOutRefId = unSpooky . txOutRefId' + +{-# INLINEABLE txOutRefIdx #-} +txOutRefIdx :: TxOutRef -> Integer +txOutRefIdx = unSpooky . txOutRefIdx' + +data ScriptPurpose + = Minting (Spooky CurrencySymbol) + | Spending (Spooky TxOutRef) + | Rewarding (Spooky StakingCredential) + | Certifying (Spooky DCert) + deriving stock (Generic, Hask.Show, Hask.Eq) +PlutusTx.unstableMakeIsData ''ScriptPurpose + +instance Eq ScriptPurpose where + {-# INLINEABLE (==) #-} + Minting cs == Minting cs' = cs == cs' + Spending ref == Spending ref' = ref == ref' + Rewarding sc == Rewarding sc' = sc == sc' + Certifying cert == Certifying cert' = cert == cert' + _ == _ = False + +data TxOut = TxOut + { txOutAddress' :: Spooky Address + , txOutValue' :: Spooky Value + , txOutDatumHash' :: Spooky (Maybe DatumHash) + } + deriving stock (Hask.Eq, Generic) +PlutusTx.unstableMakeIsData ''TxOut + +instance Eq TxOut where + {-# INLINEABLE (==) #-} + TxOut a v dh == TxOut a' v' dh' = + a == a' + && v == v' + && dh == dh' + +{-# INLINEABLE txOutAddress #-} +txOutAddress :: TxOut -> Address +txOutAddress = unSpooky . txOutAddress' + +{-# INLINEABLE txOutValue #-} +txOutValue :: TxOut -> Value +txOutValue = unSpooky . txOutValue' + +{-# INLINEABLE txOutDatumHash #-} +txOutDatumHash :: TxOut -> Maybe DatumHash +txOutDatumHash = unSpooky . txOutDatumHash' + +-- | An input of a pending transaction. +data TxInInfo = TxInInfo + { txInInfoOutRef' :: Spooky TxOutRef + , txInInfoResolved' :: Spooky TxOut + } + deriving stock (Generic, Hask.Show, Hask.Eq) + +PlutusTx.unstableMakeIsData ''TxInInfo + +instance Eq TxInInfo where + {-# INLINEABLE (==) #-} + TxInInfo a b == TxInInfo a' b' = + a == a' + && b == b' + +{-# INLINEABLE txInInfoOutRef #-} +txInInfoOutRef :: TxInInfo -> TxOutRef +txInInfoOutRef = unSpooky . txInInfoOutRef' + +{-# INLINEABLE txInInfoResolved #-} +txInInfoResolved :: TxInInfo -> TxOut +txInInfoResolved = unSpooky . txInInfoResolved' + +-- | A pending transaction. This is the view as seen by validator scripts, so some details are stripped out. +data TxInfo = TxInfo + { -- | Transaction inputs + txInfoInputs' :: Spooky [TxInInfo] + , -- | Transaction outputs + txInfoOutputs' :: Spooky [TxOut] + , -- | The fee paid by this transaction. + txInfoFee' :: Spooky Value + , -- | The 'Value' minted by this transaction. + txInfoMint' :: Spooky Value + , -- | Digests of certificates included in this transaction + txInfoDCert' :: Spooky [DCert] + , -- | Withdrawals + txInfoWdrl' :: Spooky [(StakingCredential, Integer)] + , -- | The valid range for the transaction. + txInfoValidRange' :: Spooky POSIXTimeRange + , -- | Signatures provided with the transaction, attested that they all signed the tx + txInfoSignatories' :: Spooky [PubKeyHash] + , txInfoData' :: Spooky [(DatumHash, Datum)] + , -- | Hash of the pending transaction (excluding witnesses) + txInfoId' :: Spooky TxId + } + deriving stock (Generic, Hask.Eq) + +PlutusTx.unstableMakeIsData ''TxInfo + +instance Eq TxInfo where + {-# INLINEABLE (==) #-} + TxInfo i o f m c w r s d tid == TxInfo i' o' f' m' c' w' r' s' d' tid' = + i == i' + && o == o' + && f == f' + && m == m' + && c == c' + && w == w' + && r == r' + && s == s' + && d == d' + && tid == tid' + +{-# INLINEABLE txInfoData #-} +txInfoData :: TxInfo -> [(DatumHash, Datum)] +txInfoData = unSpooky . txInfoData' + +{-# INLINEABLE txInfoSignatories #-} +txInfoSignatories :: TxInfo -> [PubKeyHash] +txInfoSignatories = unSpooky . txInfoSignatories' + +{-# INLINEABLE txInfoOutputs #-} +txInfoOutputs :: TxInfo -> [TxOut] +txInfoOutputs = unSpooky . txInfoOutputs' + +{-# INLINEABLE txInfoInputs #-} +txInfoInputs :: TxInfo -> [TxInInfo] +txInfoInputs = unSpooky . txInfoInputs' + +{-# INLINEABLE txInfoMint #-} +txInfoMint :: TxInfo -> Value +txInfoMint = unSpooky . txInfoMint' + +{-# INLINEABLE valuePaidTo #-} + +-- | Get the total value paid to a public key address by a pending transaction. +valuePaidTo :: TxInfo -> PubKeyHash -> Value +valuePaidTo ptx pkh = mconcat (pubKeyOutputsAt pkh ptx) + +{-# INLINEABLE pubKeyOutputsAt #-} + +-- | Get the values paid to a public key address by a pending transaction. +pubKeyOutputsAt :: PubKeyHash -> TxInfo -> [Value] +pubKeyOutputsAt pk p = + let flt tx = case txOutAddress tx of + (Address cred _) -> case unSpooky cred of + PubKeyCredential pk' -> if pk == unSpooky pk' then Just (txOutValue tx) else Nothing + _ -> Nothing + in mapMaybe flt (txInfoOutputs p) + +{-# INLINEABLE findDatum #-} + +-- | Find the data corresponding to a data hash, if there is one +findDatum :: DatumHash -> TxInfo -> Maybe Datum +findDatum dsh tx = snd <$> find f (txInfoData tx) + where + f (dsh', _) = dsh' == dsh + +data ScriptContext = ScriptContext + { scriptContextTxInfo' :: Spooky TxInfo + , scriptContextPurpose' :: Spooky ScriptPurpose + } + deriving stock (Generic, Hask.Eq) +PlutusTx.unstableMakeIsData ''ScriptContext + +{-# INLINEABLE scriptContextTxInfo #-} +scriptContextTxInfo :: ScriptContext -> TxInfo +scriptContextTxInfo = unSpooky . scriptContextTxInfo' + +{-# INLINEABLE scriptContextPurpose #-} +scriptContextPurpose :: ScriptContext -> ScriptPurpose +scriptContextPurpose = unSpooky . scriptContextPurpose' + +{-# INLINEABLE ownCurrencySymbol #-} +ownCurrencySymbol :: ScriptContext -> CurrencySymbol +ownCurrencySymbol context = + let purpose = scriptContextPurpose context + in case purpose of + Minting cs -> unSpooky cs + _ -> error () + +-- | The type of validators for the given connection type. +type ValidatorType (a :: Type) = DatumType a -> RedeemerType a -> ScriptContext -> Bool + +-- | Make a 'TypedValidator' from the 'CompiledCode' of a validator script and its wrapper. +mkTypedValidator :: + -- | Validator script (compiled) + PlutusTx.CompiledCode (ValidatorType a) -> + -- | A wrapper for the compiled validator + PlutusTx.CompiledCode (ValidatorType a -> WrappedValidatorType) -> + TypedValidator a +mkTypedValidator vc wrapper = + let val = Scripts.mkValidatorScript $ wrapper `PlutusTx.applyCode` vc + in unsafeCoerce $ Validators.unsafeMkTypedValidator val diff --git a/mlabs/src/Mlabs/NFT/Types.hs b/mlabs/src/Mlabs/NFT/Types.hs new file mode 100644 index 000000000..cc93be036 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Types.hs @@ -0,0 +1,748 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE UndecidableInstances #-} + +module Mlabs.NFT.Types ( + AdminContract, + AuctionBid (..), + AuctionBidParams (..), + AuctionCloseParams (..), + AuctionOpenParams (..), + AuctionState (..), + as'highestBid, + as'deadline, + as'minBid, + BuyRequestUser (..), + Content (..), + getContent, + DatumNft (..), + GenericContract, + getAppInstance, + getDatumPointer, + getDatumValue, + InformationNft (..), + info'id, + info'share, + info'author, + info'price, + info'owner, + info'auctionState, + InsertPoint (..), + instanceCurrency, + MintAct (..), + mint'nftId, + MintParams (..), + NftAppInstance (..), + appInstance'Governance, + appInstance'Address, + appInstance'UniqueToken, + appInstance'Admins, + NftAppSymbol (..), + NftId (..), + nftId'contentHash, + NftListHead (..), + head'next, + head'appInstance, + NftListNode (..), + node'information, + node'next, + node'appInstance, + nftTokenName, + Pointer (..), + pointer'assetClass, + PointInfo (..), + QueryResponse (..), + SetPriceParams (..), + Title (..), + getTitle, + UniqueToken, + UserAct (..), + act'bid, + act'newPrice, + act'symbol, + UserContract, + UserId (..), + getUserId, + UserWriter, + InitParams (..), + app'symbol, +) where + +import PlutusTx qualified +import PlutusTx.Prelude +import Prelude qualified as Hask + +import Data.Aeson (FromJSON, ToJSON) +import Data.Monoid (Last (..)) +import Data.Text (Text) +import GHC.Generics (Generic) + +import Ledger ( + ChainIndexTxOut, + POSIXTime, + -- PaymentPubKeyHash, + -- PubKeyHash, + TxOutRef, + ) +import Plutus.ChainIndex (ChainIndexTx) +import Plutus.Contract (Contract) + +import Mlabs.NFT.Spooky ( + Address, + AssetClass (..), + CurrencySymbol, + PaymentPubKeyHash, + PubKeyHash, + Spooky, + TokenName (..), + toSpooky, + unAssetClass, + unSpooky, + ) +import Schema (ToSchema) + +-------------------------------------------------------------------------------- +-- ON-CHAIN TYPES -- + +newtype Content = Content {getContent' :: Spooky BuiltinByteString} + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.unstableMakeIsData ''Content +PlutusTx.makeLift ''Content + +instance Eq Content where + {-# INLINEABLE (==) #-} + (Content c1) == (Content c2) = c1 == c2 + +{-# INLINEABLE getContent #-} +getContent :: Content -> BuiltinByteString +getContent = unSpooky . getContent' + +newtype Title = Title {getTitle' :: Spooky BuiltinByteString} + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.unstableMakeIsData ''Title +PlutusTx.makeLift ''Title + +instance Eq Title where + {-# INLINEABLE (==) #-} + (Title t1) == (Title t2) = t1 == t2 + +{-# INLINEABLE getTitle #-} +getTitle :: Title -> BuiltinByteString +getTitle = unSpooky . getTitle' + +newtype UserId = UserId {getUserId' :: Spooky PaymentPubKeyHash} + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.unstableMakeIsData ''UserId +PlutusTx.makeLift ''UserId + +instance Eq UserId where + {-# INLINEABLE (==) #-} + (UserId u1) == (UserId u2) = u1 == u2 + +instance Ord UserId where + {-# INLINEABLE (<=) #-} + (UserId u1) <= (UserId u2) = unSpooky @PaymentPubKeyHash u1 <= unSpooky u2 + +instance Hask.Ord UserId where + (UserId u1) <= (UserId u2) = unSpooky @PaymentPubKeyHash u1 <= unSpooky u2 + +{-# INLINEABLE getUserId #-} +getUserId :: UserId -> PaymentPubKeyHash +getUserId = unSpooky . getUserId' + +{- | Unique identifier of NFT. + The NftId contains a human readable title, the hashed information of the + content and the utxo ref included when minting the token. +-} +newtype NftId = NftId + { -- | token name is identified by content of the NFT (it's hash of it). + nftId'contentHash' :: Spooky BuiltinByteString + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +PlutusTx.unstableMakeIsData ''NftId +PlutusTx.makeLift ''NftId + +instance Eq NftId where + {-# INLINEABLE (==) #-} + (NftId token1) == (NftId token2) = + token1 == token2 + +instance Ord NftId where + {-# INLINEABLE (<=) #-} + (NftId token1) <= (NftId token2) = + unSpooky @BuiltinByteString token1 <= unSpooky token2 + +instance Hask.Ord NftId where + (NftId token1) <= (NftId token2) = + unSpooky @BuiltinByteString token1 <= unSpooky token2 + +{-# INLINEABLE nftId'contentHash #-} +nftId'contentHash :: NftId -> BuiltinByteString +nftId'contentHash = unSpooky . nftId'contentHash' + +-- | Minting Policy Redeemer +data MintAct + = -- | Mint Action for the NftId. Creates a proof token that the NFTid is + -- unique. + Mint + { -- | NftId + mint'nftId' :: Spooky NftId + } + | -- | Create the Datum. + Initialise + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.unstableMakeIsData ''MintAct +PlutusTx.makeLift ''MintAct + +{-# INLINEABLE mint'nftId #-} +mint'nftId :: MintAct -> NftId +mint'nftId Mint {..} = unSpooky mint'nftId' +mint'nftId _ = error () + +-------------------------------------------------------------------------------- +-- ENDPOINTS PARAMETERS -- + +-- | Parameters for initialising NFT marketplace +data InitParams = InitParams + { -- | List of app admins + ip'admins :: [UserId] + , -- | Fee rate of transaction + ip'feeRate :: Rational + , -- | PKH where fee is sent + ip'feePkh :: PubKeyHash + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +PlutusTx.makeLift ''InitParams +PlutusTx.unstableMakeIsData ''InitParams + +instance Eq InitParams where + (InitParams admins1 feeRate1 feePkh1) == (InitParams admins2 feeRate2 feePkh2) = + admins1 == admins2 && feeRate1 == feeRate2 && feePkh1 == feePkh2 + +-- | Parameters that need to be submitted when minting a new NFT. +data MintParams = MintParams + { -- | Content to be minted. + mp'content :: Content + , -- | Title of content. + mp'title :: Title + , -- | Shares retained by author. + mp'share :: Rational + , -- | Listing price of the NFT, in Lovelace. + mp'price :: Maybe Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +PlutusTx.makeLift ''MintParams +PlutusTx.unstableMakeIsData ''MintParams + +instance Eq MintParams where + {-# INLINEABLE (==) #-} + (MintParams content1 title1 share1 price1) == (MintParams content2 title2 share2 price2) = + content1 == content2 && title1 == title2 && share1 == share2 && price1 == price2 + +data SetPriceParams = SetPriceParams + { -- | NFTid of the token which price is set. + sp'nftId :: NftId + , -- | New price, in Lovelace. + sp'price :: Maybe Integer -- todo maybe Natural? are they available here? + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +instance Eq SetPriceParams where + {-# INLINEABLE (==) #-} + (SetPriceParams nftId1 price1) == (SetPriceParams nftId2 price2) = + nftId1 == nftId2 && price1 == price2 + +data BuyRequestUser = BuyRequestUser + { -- | nftId to Buy + ur'nftId :: NftId + , -- | price to buy, in Lovelace. + ur'price :: Integer + , -- | new price for NFT (Nothing locks NFT), in Lovelace. + ur'newPrice :: Maybe Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.makeLift ''BuyRequestUser +PlutusTx.unstableMakeIsData ''BuyRequestUser + +instance Eq BuyRequestUser where + {-# INLINEABLE (==) #-} + (BuyRequestUser nftId1 price1 newPrice1) == (BuyRequestUser nftId2 price2 newPrice2) = + nftId1 == nftId2 && price1 == price2 && newPrice1 == newPrice2 + +data AuctionOpenParams = AuctionOpenParams + { -- | nftId + op'nftId :: NftId + , -- | Auction deadline + op'deadline :: POSIXTime + , -- | Auction minimum bid in lovelace + op'minBid :: Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.unstableMakeIsData ''AuctionOpenParams +PlutusTx.makeLift ''AuctionOpenParams + +instance Eq AuctionOpenParams where + {-# INLINEABLE (==) #-} + (AuctionOpenParams nftId1 deadline1 minBid1) == (AuctionOpenParams nftId2 deadline2 minBid2) = + nftId1 == nftId2 && deadline1 == deadline2 && minBid1 == minBid2 + +data AuctionBidParams = AuctionBidParams + { -- | nftId + bp'nftId :: NftId + , -- | Bid amount in lovelace + bp'bidAmount :: Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.unstableMakeIsData ''AuctionBidParams +PlutusTx.makeLift ''AuctionBidParams + +instance Eq AuctionBidParams where + {-# INLINEABLE (==) #-} + (AuctionBidParams nftId1 bid1) == (AuctionBidParams nftId2 bid2) = + nftId1 == nftId2 && bid1 == bid2 + +newtype AuctionCloseParams = AuctionCloseParams + { -- | nftId + cp'nftId :: NftId + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) +PlutusTx.unstableMakeIsData ''AuctionCloseParams +PlutusTx.makeLift ''AuctionCloseParams + +instance Eq AuctionCloseParams where + {-# INLINEABLE (==) #-} + (AuctionCloseParams nftId1) == (AuctionCloseParams nftId2) = + nftId1 == nftId2 + +-------------------------------------------------------------------------------- +-- Validation + +data AuctionBid = AuctionBid + { -- | Bid in Lovelace + ab'bid' :: Spooky Integer + , -- | Bidder's wallet pubkey + ab'bidder' :: Spooky UserId + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) +PlutusTx.unstableMakeIsData ''AuctionBid +PlutusTx.makeLift ''AuctionBid + +instance Eq AuctionBid where + {-# INLINEABLE (==) #-} + (AuctionBid bid1 bidder1) == (AuctionBid bid2 bidder2) = + bid1 == bid2 && bidder1 == bidder2 + +data AuctionState = AuctionState + { -- | Highest bid + as'highestBid' :: Spooky (Maybe AuctionBid) + , -- | Deadline + as'deadline' :: Spooky POSIXTime + , -- | Minimum bid amount + as'minBid' :: Spooky Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) +PlutusTx.unstableMakeIsData ''AuctionState +PlutusTx.makeLift ''AuctionState + +instance Eq AuctionState where + {-# INLINEABLE (==) #-} + (AuctionState bid1 deadline1 minBid1) == (AuctionState bid2 deadline2 minBid2) = + bid1 == bid2 && deadline1 == deadline2 && minBid1 == minBid2 + +{-# INLINEABLE as'highestBid #-} +as'highestBid :: AuctionState -> Maybe AuctionBid +as'highestBid = unSpooky . as'highestBid' + +{-# INLINEABLE as'deadline #-} +as'deadline :: AuctionState -> POSIXTime +as'deadline = unSpooky . as'deadline' + +{-# INLINEABLE as'minBid #-} +as'minBid :: AuctionState -> Integer +as'minBid = unSpooky . as'minBid' + +-- | NFT Information. +data InformationNft = InformationNft + { -- | NFT ID. Represents the key of the Datum. ?could even be taken out of the information? + info'id' :: Spooky NftId + , -- | Author's share of the NFT. + info'share' :: Spooky Rational + , -- | Author's wallet pubKey. + info'author' :: Spooky UserId + , -- | Owner's wallet pubkey. + info'owner' :: Spooky UserId + , -- | Price in Lovelace. If Nothing, NFT not for sale. + info'price' :: Spooky (Maybe Integer) + , -- | Auction state + info'auctionState' :: Spooky (Maybe AuctionState) + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.unstableMakeIsData ''InformationNft +PlutusTx.makeLift ''InformationNft + +{-# INLINEABLE info'id #-} +info'id :: InformationNft -> NftId +info'id = unSpooky . info'id' + +{-# INLINEABLE info'share #-} +info'share :: InformationNft -> Rational +info'share = unSpooky . info'share' + +{-# INLINEABLE info'author #-} +info'author :: InformationNft -> UserId +info'author = unSpooky . info'author' + +{-# INLINEABLE info'owner #-} +info'owner :: InformationNft -> UserId +info'owner = unSpooky . info'owner' + +{-# INLINEABLE info'price #-} +info'price :: InformationNft -> Maybe Integer +info'price = unSpooky . info'price' + +{-# INLINEABLE info'auctionState #-} +info'auctionState :: InformationNft -> Maybe AuctionState +info'auctionState = unSpooky . info'auctionState' + +instance Ord InformationNft where + {-# INLINEABLE (<=) #-} + x <= y = info'id x <= info'id y + +instance Eq InformationNft where + {-# INLINEABLE (==) #-} + (InformationNft a b c d e f) == (InformationNft a' b' c' d' e' f') = + a == a' && b == b' && c == c' && d == d' && e == e' && f == f' + +-- | Unique Token is an AssetClass. +type UniqueToken = AssetClass + +{- | App Instace is parametrised by the Unique Token located in the head of the + list. +-} +data NftAppInstance = NftAppInstance + { -- | Script Address where all the NFTs can be found + appInstance'Address' :: Spooky Address + , -- | AssetClass with which all the NFTs are parametrised - guarantees the proof of uniqueness. + appInstance'UniqueToken' :: Spooky UniqueToken + , -- | Governance Address + appInstance'Governance' :: Spooky Address + , -- | List of admins who can initiate the application + appInstance'Admins' :: Spooky [UserId] + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.unstableMakeIsData ''NftAppInstance +PlutusTx.makeLift ''NftAppInstance + +instance Eq NftAppInstance where + {-# INLINEABLE (==) #-} + (NftAppInstance a b c d) == (NftAppInstance a' b' c' d') = a == a' && b == b' && c == c' && d == d' + +{-# INLINEABLE appInstance'Address #-} +appInstance'Address :: NftAppInstance -> Address +appInstance'Address = unSpooky . appInstance'Address' + +{-# INLINEABLE appInstance'UniqueToken #-} +appInstance'UniqueToken :: NftAppInstance -> UniqueToken +appInstance'UniqueToken = unSpooky . appInstance'UniqueToken' + +{-# INLINEABLE appInstance'Governance #-} +appInstance'Governance :: NftAppInstance -> Address +appInstance'Governance = unSpooky . appInstance'Governance' + +{-# INLINEABLE appInstance'Admins #-} +appInstance'Admins :: NftAppInstance -> [UserId] +appInstance'Admins = unSpooky . appInstance'Admins' + +-- | Get `CurrencySumbol` of `NftAppInstance` +instanceCurrency :: NftAppInstance -> CurrencySymbol +instanceCurrency = fst . unAssetClass . appInstance'UniqueToken + +newtype NftAppSymbol = NftAppSymbol {app'symbol' :: Spooky CurrencySymbol} + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) +PlutusTx.unstableMakeIsData ''NftAppSymbol +PlutusTx.makeLift ''NftAppSymbol + +instance Eq NftAppSymbol where + {-# INLINEABLE (==) #-} + (NftAppSymbol a) == (NftAppSymbol a') = a == a' + +{-# INLINEABLE app'symbol #-} +app'symbol :: NftAppSymbol -> CurrencySymbol +app'symbol = unSpooky . app'symbol' + +{- | The AssetClass is the pointer itself. Each NFT has the same CurrencySymbol, + and their TokenName is the Hash of their Content. +-} +newtype Pointer = Pointer + { pointer'assetClass' :: Spooky AssetClass + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.unstableMakeIsData ''Pointer +PlutusTx.makeLift ''Pointer + +instance Eq Pointer where + {-# INLINEABLE (==) #-} + (Pointer a) == (Pointer a') = a == a' + +instance Ord Pointer where + {-# INLINEABLE compare #-} + a `compare` a' = pointer'assetClass a `compare` pointer'assetClass a' + +{-# INLINEABLE pointer'assetClass #-} +pointer'assetClass :: Pointer -> AssetClass +pointer'assetClass = unSpooky . pointer'assetClass' + +{- | The head datum is unique for each list. Its token is minted when the unique + NFT is consumed. +-} +data NftListHead = NftListHead + { -- | Pointer to the next node. + head'next' :: Spooky (Maybe Pointer) + , -- | Node App Instance + head'appInstance' :: Spooky NftAppInstance + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.unstableMakeIsData ''NftListHead +PlutusTx.makeLift ''NftListHead + +{-# INLINEABLE head'next #-} +head'next :: NftListHead -> Maybe Pointer +head'next = unSpooky . head'next' + +{-# INLINEABLE head'appInstance #-} +head'appInstance :: NftListHead -> NftAppInstance +head'appInstance = unSpooky . head'appInstance' + +instance Eq NftListHead where + {-# INLINEABLE (==) #-} + (NftListHead a b) == (NftListHead a' b') = a == a' && b == b' + +-- | The nft list node is based on the above described properties. +data NftListNode = NftListNode + { -- | The value held at the node + node'information' :: Spooky InformationNft + , -- | The next Node. + node'next' :: Spooky (Maybe Pointer) + , -- | Node App Instance + node'appInstance' :: Spooky NftAppInstance + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) + +instance Ord NftListNode where + {-# INLINEABLE (<=) #-} + x <= y = node'information x <= node'information y + +{-# INLINEABLE node'information #-} +node'information :: NftListNode -> InformationNft +node'information = unSpooky . node'information' + +{-# INLINEABLE node'next #-} +node'next :: NftListNode -> Maybe Pointer +node'next = unSpooky . node'next' + +{-# INLINEABLE node'appInstance #-} +node'appInstance :: NftListNode -> NftAppInstance +node'appInstance = unSpooky . node'appInstance' + +PlutusTx.unstableMakeIsData ''NftListNode +PlutusTx.makeLift ''NftListNode +instance Eq NftListNode where + {-# INLINEABLE (==) #-} + (NftListNode a b c) == (NftListNode a' b' c') = a == a' && b == b' && c == c' + +-- | The datum of an Nft is either head or node. +data DatumNft + = -- | Head of a List + HeadDatum NftListHead + | -- | A node of the list. + NodeDatum NftListNode + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) + +instance Ord DatumNft where + {-# INLINEABLE (<=) #-} + (HeadDatum _) <= _ = True + (NodeDatum _) <= (HeadDatum _) = False + (NodeDatum x) <= (NodeDatum y) = x <= y + +PlutusTx.unstableMakeIsData ''DatumNft +PlutusTx.makeLift ''DatumNft + +-- | Pointer to the next List Item. +getDatumPointer :: DatumNft -> Maybe Pointer +getDatumPointer = \case + HeadDatum listHead -> head'next listHead + NodeDatum listNode -> node'next listNode + +getDatumValue :: DatumNft -> BuiltinByteString +getDatumValue = \case + HeadDatum _ -> "" + NodeDatum listNode -> nftId'contentHash . info'id . node'information $ listNode + +instance Eq DatumNft where + {-# INLINEABLE (==) #-} + (HeadDatum x1) == (HeadDatum x2) = x1 == x2 + (NodeDatum x1) == (NodeDatum x2) = x1 == x2 + _ == _ = False + +{- | Token Name is represented by the HASH of the artwork. The Head TokenName is +the empty ByteString, smaller than any other ByteString, and is minted at the +intialisation of the app. +-} +nftTokenName :: DatumNft -> TokenName +nftTokenName = \case + HeadDatum _ -> TokenName . toSpooky $ PlutusTx.Prelude.emptyByteString + NodeDatum n -> TokenName . toSpooky . nftId'contentHash . info'id . node'information $ n + +getAppInstance :: DatumNft -> NftAppInstance +getAppInstance = \case + HeadDatum h -> head'appInstance h + NodeDatum n -> node'appInstance n + +-- | NFT Redeemer +data UserAct + = -- | Buy NFT and set new price + BuyAct + { -- | price to buy. In Lovelace. + act'bid' :: Spooky Integer + , -- | new price for NFT. In Lovelace. + act'newPrice' :: Spooky (Maybe Integer) + , -- | Nft symbol + act'symbol' :: Spooky NftAppSymbol + } + | -- | Set new price for NFT + SetPriceAct + { -- | new price for NFT. In Lovelace. + act'newPrice' :: Spooky (Maybe Integer) + , -- | Nft symbol + act'symbol' :: Spooky NftAppSymbol + } + | -- | Mint a new Unique NFT. + MintAct + { -- | NFT. + act'nftId' :: Spooky NftId + , -- | Nft symbol + act'symbol' :: Spooky NftAppSymbol + } + | -- | Start NFT auction + OpenAuctionAct + { -- | Nft symbol + act'symbol' :: Spooky NftAppSymbol + } + | -- | Make a bid in an auction + BidAuctionAct + { -- | Bid amount in lovelace + act'bid' :: Spooky Integer + , -- | Nft symbol + act'symbol' :: Spooky NftAppSymbol + } + | CloseAuctionAct + { -- | Nft symbol + act'symbol' :: Spooky NftAppSymbol + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.makeLift ''UserAct +PlutusTx.unstableMakeIsData ''UserAct + +instance Eq UserAct where + {-# INLINEABLE (==) #-} + (BuyAct bid1 newPrice1 symbol1) == (BuyAct bid2 newPrice2 symbol2) = + bid1 == bid2 && newPrice1 == newPrice2 && symbol1 == symbol2 + (SetPriceAct newPrice1 symbol1) == (SetPriceAct newPrice2 symbol2) = + newPrice1 == newPrice2 && symbol1 == symbol2 + _ == _ = False + +{-# INLINEABLE act'bid #-} +act'bid :: UserAct -> Integer +act'bid (BuyAct bid _ _) = unSpooky bid +act'bid (BidAuctionAct bid _) = unSpooky bid +act'bid _ = error () + +{-# INLINEABLE act'newPrice #-} +act'newPrice :: UserAct -> Maybe Integer +act'newPrice (BuyAct _ newPrice _) = unSpooky newPrice +act'newPrice (SetPriceAct newPrice _) = unSpooky newPrice +act'newPrice _ = error () + +{-# INLINEABLE act'symbol #-} +act'symbol :: UserAct -> NftAppSymbol +act'symbol (BuyAct _ _ symbol) = unSpooky symbol +act'symbol (SetPriceAct _ symbol) = unSpooky symbol +act'symbol (MintAct _ symbol) = unSpooky symbol +act'symbol (OpenAuctionAct symbol) = unSpooky symbol +act'symbol (BidAuctionAct _ symbol) = unSpooky symbol +act'symbol (CloseAuctionAct symbol) = unSpooky symbol + +-- OffChain utility types. + +-- | A datatype used by the QueryContract to return a response +data QueryResponse + = QueryCurrentOwner (Maybe UserId) + | QueryCurrentPrice (Maybe Integer) + | QueryContent (Maybe InformationNft) + | QueryListNfts [InformationNft] + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +-- | Easy type to find and use Nodes by. +data PointInfo a = PointInfo + { pi'data :: a + , pi'TOR :: Ledger.TxOutRef + , pi'CITxO :: ChainIndexTxOut + , pi'CITx :: ChainIndexTx + } + deriving stock (Hask.Eq, Hask.Show) + +instance Eq a => Eq (PointInfo a) where + {-# INLINEABLE (==) #-} + (PointInfo x y _ _) == (PointInfo a b _ _) = + x == a && y == b -- && z == c && k == d + +instance Ord a => Ord (PointInfo a) where + {-# INLINEABLE (<=) #-} + x <= y = pi'data x <= pi'data y + +instance (Ord a, Hask.Eq a) => Hask.Ord (PointInfo a) where + x <= y = pi'data x <= pi'data y + +-- | Two positions in on-chain list between which new NFT will be "inserted" +data InsertPoint a = InsertPoint + { prev :: PointInfo a + , next :: Maybe (PointInfo a) + } + deriving stock (Hask.Show) + +-- Contract types +type GenericContract a = forall w s. Contract w s Text a +type UserWriter = Last (Either NftId QueryResponse) +type UserContract s a = Contract UserWriter s Text a +type AdminContract s a = Contract (Last NftAppInstance) s Text a diff --git a/mlabs/src/Mlabs/NFT/Validation.hs b/mlabs/src/Mlabs/NFT/Validation.hs new file mode 100644 index 000000000..6cb9ec0a3 --- /dev/null +++ b/mlabs/src/Mlabs/NFT/Validation.hs @@ -0,0 +1,846 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE UndecidableInstances #-} +-- FIXME: Remove after uncommenting commented parts +{-# OPTIONS_GHC -Wno-unused-imports #-} + +module Mlabs.NFT.Validation ( + DatumNft (..), + NftTrade, + calculateShares, + UserAct (..), + asRedeemer, + txPolicy, + mkTxPolicy, + txScrAddress, + txValHash, + nftCurrency, + nftAsset, + mintPolicy, + mkMintPolicy, + priceNotNegative, + curSymbol, +) where + +import PlutusTx qualified +import PlutusTx.Prelude + +import Ledger ( + Datum (..), + MintingPolicy, + Redeemer (..), + ValidatorHash, + contains, + findOwnInput, + from, + mkMintingPolicyScript, + mkValidatorScript, + scriptCurrencySymbol, + to, + ) +import Ledger.Typed.Scripts ( + DatumType, + RedeemerType, + TypedValidator, + ValidatorTypes, + WrappedMintingPolicyType, + mkTypedValidator, + unsafeMkTypedValidator, + validatorAddress, + validatorHash, + wrapMintingPolicy, + ) +import Ledger.Typed.TypeUtils (Any) +import PlutusTx.Ratio qualified as R + +import Data.Function (on) +import Data.Maybe (catMaybes) +import Data.Tuple.Extra (uncurry3) + +import Mlabs.NFT.Governance.Types ( + GovDatum (gov'list), + GovLHead (GovLHead, govLHead'feeRate, govLHead'pkh), + LList (HeadLList, _head'info, _head'next), + ) +import Mlabs.NFT.Spooky ( + Address, + AssetClass (AssetClass), + CurrencySymbol, + ScriptContext, + TokenName (TokenName), + TxOut, + Value, + adaSymbol, + adaToken, + assetClass, + assetClassValueOf, + findDatum, + flattenValue, + lovelaceValueOf, + ownCurrencySymbol, + scriptContextTxInfo, + singleton, + toSpooky, + toSpookyAddress, + toSpookyCurrencySymbol, + toSpookyTokenName, + txInInfoResolved, + txInfoInputs, + txInfoMint, + txInfoOutputs, + txInfoSignatories, + txOutAddress, + txOutDatumHash, + txOutValue, + unAssetClass, + unPaymentPubKeyHash, + unSpookyCurrencySymbol, + unSpookyTokenName, + unTokenName, + valueOf, + valuePaidTo, + ) +import Mlabs.NFT.Types ( + DatumNft (..), + InformationNft (info'author', info'price'), + MintAct (Initialise, Mint), + NftAppInstance, + NftId (NftId), + NftListHead, + NftListNode (node'information'), + Pointer, + UniqueToken, + UserAct (..), + UserId (UserId), + act'bid, + act'newPrice, + act'symbol, + app'symbol, + appInstance'Address, + appInstance'Admins, + appInstance'UniqueToken, + getAppInstance, + getDatumPointer, + getUserId, + head'appInstance, + info'author, + info'id, + info'owner, + info'price, + info'share, + mint'nftId, + nftId'contentHash, + nftTokenName, + node'appInstance, + node'information, + pointer'assetClass, + ) + +asRedeemer :: PlutusTx.ToData a => a -> Redeemer +asRedeemer = Redeemer . PlutusTx.toBuiltinData + +{-# INLINEABLE mkMintPolicy #-} + +-- | Minting policy for NFTs. +mkMintPolicy :: NftAppInstance -> MintAct -> ScriptContext -> Bool +mkMintPolicy !appInstance !act !ctx = + case act of + mintAct@Mint {} -> + traceIfFalse' "Only pointer of first node can change." firstChangedOnlyPtr + && traceIfFalse' "Exactly one NFT must be minted" (checkMintedAmount . mint'nftId $ mintAct) + && traceIfFalse' "Old first node must point to second node." (first `pointsTo'` second) + && traceIfFalse' "New first node must point to new node." (newFirst `pointsTo` newInserted) + && traceIfFalse' "New node must point to second node." (newInserted `pointsTo'` second) + && traceIfFalse' "New node must be smaller than second node." newIsSmallerThanSecond + && traceIfFalse' "New price cannot be negative." priceNotNegative' + && traceIfFalse' "Currency symbol must match app instance" checkCurrencySymbol + && traceIfFalse' "Minted token must be sent to script address" (checkSentAddress . mint'nftId $ mintAct) + && traceIfFalse' "Nodes must be sent to script address" checkNodesAddresses + && traceIfFalse' "Datum is not atttached to UTXo with correct Token" (checkAttachedDatum . mint'nftId $ mintAct) + Initialise -> + traceIfFalse' "The token is not present." headTokenIsPresent + && traceIfFalse' "Only one Unique Token can be minted" headTokenIsUnique + && traceIfFalse' "The token is not sent to the right address" headTokenToRightAddress + && traceIfFalse' "Only an admin can initialise app." checkAdminSig + where + ------------------------------------------------------------------------------ + -- Helpers + + !info = scriptContextTxInfo ctx + !scriptAddress = appInstance'Address appInstance + + sentToScript tx = txOutAddress tx == scriptAddress + + sort2 (x, y) = if x < y then (x, y) else (y, x) + + (newFirst, newInserted) = case getOutputDatums ctx of + [x, y] -> sort2 (x, y) + [_] -> traceError' "Expected exactly two outputs with datums. Receiving one." + [] -> traceError' "Expected exactly two outputs with datums. Receiving none." + _ -> traceError' "Expected exactly two outputs with datums. Receiving more." + first = case getInputDatums ctx of + [x] -> x + [] -> traceError' "Expected exactly one input with datums. Receiving none." + _ -> traceError' "Expected exactly one input with datums. Receiving more." + second = getDatumPointer first + + pointsTo d1 d2 = case (d1, d2) of + (_, NodeDatum _) -> case getDatumPointer d1 of + Just ptr -> (== nftTokenName d2) . snd . unAssetClass . pointer'assetClass $ ptr + Nothing -> False + _ -> False + + pointsTo' :: DatumNft -> Maybe Pointer -> Bool + pointsTo' !datum !pointer = getDatumPointer datum == pointer + + ------------------------------------------------------------------------------ + -- Checks + + -- Check if nodes are sent back to script address + checkNodesAddresses = + let txs :: [TxOut] = + fmap snd + . getOutputDatumsWithTx @DatumNft + $ ctx + in all sentToScript txs + + -- Check if price is positive + priceNotNegative' = case newInserted of + NodeDatum node -> priceNotNegative (info'price . node'information $ node) + _ -> False + + -- Check if minted NFT is sent to script address + checkSentAddress nftId = + let currency = ownCurrencySymbol ctx + tokenName = TokenName . toSpooky . nftId'contentHash $ nftId + txOut = find (\tx -> valueOf (txOutValue tx) currency tokenName == 1) $ txInfoOutputs info + in maybe False sentToScript txOut + + newIsSmallerThanSecond = case second of + Nothing -> True + Just ptr -> (> nftTokenName newInserted) . snd . unAssetClass . pointer'assetClass $ ptr + + -- Check if currency symbol is consistent + checkCurrencySymbol = + getAppInstance first == appInstance + && getAppInstance newInserted == appInstance + + -- Check if minting only one token + checkMintedAmount nftid = + let currency = ownCurrencySymbol ctx + tokenName = TokenName . toSpooky . nftId'contentHash $ nftid + in txInfoMint info == singleton currency tokenName 1 + + -- Check if only thing changed in first node is `next` pointer + firstChangedOnlyPtr = case (first, newFirst) of + (NodeDatum node1, NodeDatum node2) -> + node'appInstance node1 == node'appInstance node2 + && node'information node1 == node'information node2 + (HeadDatum node1, HeadDatum node2) -> + head'appInstance node1 == head'appInstance node2 + _ -> False + + -- Check if Datum and Token id matches + checkAttachedDatum nftId = + let snd3 (_, y, _) = y + mintedId = + NftId + . toSpooky + . unTokenName + . snd3 + . head + . flattenValue + . txInfoMint + . scriptContextTxInfo + $ ctx + in case newInserted of + HeadDatum _ -> False + NodeDatum node -> + let datumId = info'id . node'information $ node + in mintedId == datumId && datumId == nftId + + !outputsWithHeadDatum = + filter + ( \(datum, _) -> + case datum of + HeadDatum _ -> True + _ -> False + ) + $ getOutputDatumsWithTx ctx + + -- Check if the head token is present + headTokenIsPresent = + let validValue (sym, _, _) = sym == ownCurrencySymbol ctx + validHeadToken tx = any validValue $ flattenValue . txOutValue $ tx + in any (validHeadToken . snd) outputsWithHeadDatum + + -- Check if the head token is spent to the right address + headTokenToRightAddress = + let validValue (sym, _, _) = sym == ownCurrencySymbol ctx + validHeadToken tx = + sentToScript tx + && any validValue (flattenValue . txOutValue $ tx) + in any (validHeadToken . snd) outputsWithHeadDatum + + -- Check the uniqueness of minted head token + headTokenIsUnique = + let validValue (sym, _, v) = sym == ownCurrencySymbol ctx && v == 1 + validHeadToken tx = + sentToScript tx + && any validValue (flattenValue . txOutValue $ tx) + in any (validHeadToken . snd) outputsWithHeadDatum + + -- Check an admin signed the transaction + checkAdminSig = + let admins = appInstance'Admins appInstance + in any (`elem` admins) $ UserId . toSpooky <$> txInfoSignatories info + +mintPolicy :: NftAppInstance -> MintingPolicy +mintPolicy appInstance = + mkMintingPolicyScript $ + $$(PlutusTx.compile [||myWrapMintingPolicy . mkMintPolicy||]) + `PlutusTx.applyCode` PlutusTx.liftCode appInstance + +{-# INLINEABLE mkTxPolicy #-} + +-- | A validator script for the user actions. +mkTxPolicy :: UniqueToken -> DatumNft -> UserAct -> ScriptContext -> Bool +mkTxPolicy _ !datum' !act !ctx = + case act of + MintAct {} -> case datum' of + NodeDatum _ -> + traceIfFalse' "Transaction can only use one NftListNode element as uniqueness proof." onlyOneNodeAttached + && traceIfFalse' "Not all used tokens are returned." checkTokenReturned + && traceIfFalse' "Returned Token UTXOs have mismatching datums." checkMissMatchDatumMint + HeadDatum headDat -> + -- must always pay back the proof Token. This happens when the Head datum is + -- updated as the utxo needs to be consumed + traceIfFalse' "Proof Token must be paid back when using Head" proofPaidBack + && traceIfFalse' "Transaction that uses Head as list proof must return it unchanged." headUnchanged + where + oldHead :: NftListHead = case mapMaybe getHead . getInputDatums $ ctx of + [] -> traceError' "oldHead: Head not found" + [h] -> h + _ -> traceError' "oldHead: More than one head" + + !proofPaidBack = any paysBack . txInfoOutputs . scriptContextTxInfo $ ctx + where + (currency, tokenName) = unAssetClass . appInstance'UniqueToken . head'appInstance $ headDat + paysBack tx = valueOf (txOutValue tx) currency tokenName == 1 + !headUnchanged = oldHead == headDat + buyAct@BuyAct {} -> case datum' of + NodeDatum node -> + traceIfFalse' "Transaction cannot mint." noMint + && traceIfFalse' "NFT not for sale." nftForSale + && traceIfFalse' "New Price cannot be negative." (priceNotNegative . act'newPrice $ buyAct) + && traceIfFalse' "Act'Bid is too low for the NFT price." (bidHighEnough . act'bid $ buyAct) + && traceIfFalse' "Datum is not consistent, illegally altered." (consistentDatumBuy node) + && traceIfFalse' "Only one Node must be used in a Buy Action." onlyOneNodeAttached + && traceIfFalse' "Not all used Tokens are returned." checkTokenReturned + && traceIfFalse' "Returned Token UTXO has mismatching datum." checkMissMatchDatum + && if ownerIsAuthor + then traceIfFalse' "Amount paid to author/owner does not match" (correctPaymentOnlyAuthor node . act'bid $ buyAct) + else + traceIfFalse' "Current owner is not paid their share." (correctPaymentOwner node . act'bid $ buyAct) + && traceIfFalse' "Author is not paid their share." (correctPaymentAuthor node . act'bid $ buyAct) + HeadDatum _ -> False + setPriceAct@SetPriceAct {} -> case datum' of + NodeDatum node -> + traceIfFalse' "Transaction cannot mint." noMint + && traceIfFalse' "Datum does not correspond to NFTId, no datum is present, or more than one suitable datums are present." (correctDatumSetPrice node) + && traceIfFalse' "New Price cannot be negative." (priceNotNegative . act'newPrice $ setPriceAct) + && traceIfFalse' "Only owner exclusively can set NFT price." (signedByOwner node) + && traceIfFalse' "Datum is not consistent, illegally altered." (consistentDatumSetPrice node) + && traceIfFalse' "Only one Node must be used in a SetPrice Action." onlyOneNodeAttached + && traceIfFalse' "Not all used Tokens are returned." checkTokenReturned + && traceIfFalse' "Returned Token UTXO has mismatching datum." checkMissMatchDatum + -- && traceIfFalse' "NFT is on auction" (checkIsNotOnAuction node) + HeadDatum _ -> False + _ -> False + where + -- OpenAuctionAct {} -> case datum' of + -- NodeDatum node -> + -- traceIfFalse' "Can't open auction: already in progress" (noAuctionInProgress node) + -- && traceIfFalse' "Only owner can open auction" (signedByOwner node) + -- && traceIfFalse' "Open Auction: datum illegally altered" (auctionConsistentOpenDatum node) + -- && traceIfFalse' "NFT price must be set to Nothing" checkPriceIsNothing + -- HeadDatum _ -> False + -- BidAuctionAct {..} -> case datum' of + -- NodeDatum node -> + -- traceIfFalse' "Can't bid: No auction is in progress" (not $ noAuctionInProgress node) + -- && traceIfFalse' "Auction bid is too low" (auctionBidHighEnough node act'bid) + -- && traceIfFalse' "Auction deadline reached" (correctAuctionBidSlotInterval node) + -- && traceIfFalse' "Auction: wrong input value" (correctInputValue node) + -- && traceIfFalse' "Bid Auction: datum illegally altered" (auctionConsistentDatum node act'bid) + -- && traceIfFalse' "Auction bid value not supplied" (auctionBidValueSupplied act'bid) + -- && traceIfFalse' "Incorrect bid refund" (correctBidRefund node) + -- HeadDatum _ -> False + -- CloseAuctionAct {} -> case datum' of + -- NodeDatum node -> + -- traceIfFalse' "Can't close auction: none in progress" (not $ noAuctionInProgress node) + -- && traceIfFalse' "Auction deadline not yet reached" (auctionDeadlineReached node) + -- && traceIfFalse' "Auction: new owner set incorrectly" (auctionCorrectNewOwner node) + -- && traceIfFalse' "Close Auction: datum illegally altered" (auctionConsistentCloseDatum node) + -- && if ownerIsAuthor + -- then traceIfFalse' "Auction: amount paid to author/owner does not match bid" (auctionCorrectPaymentOnlyAuthor node) + -- else + -- traceIfFalse' "Auction: owner not paid their share" (auctionCorrectPaymentOwner node) + -- && traceIfFalse' "Auction: author not paid their share" (auctionCorrectPaymentAuthor node) + -- HeadDatum _ -> False + + info = scriptContextTxInfo ctx + + !nInfo = node'information + + oldNode :: NftListNode = case mapMaybe getNode . getInputDatums $ ctx of + [n] -> n + _ -> traceError' "Input datum not found." + + -- mauctionState = info'auctionState . nInfo + + -- tokenValue :: Value + -- tokenValue = singleton (app'symbol . act'symbol $ act) (nftTokenName datum') 1 + + ------------------------------------------------------------------------------ + -- Utility functions. + nftCurr = app'symbol . act'symbol $ act + + feeRate :: Rational + feeRate = R.reduce 5 1000 + -- | [GovLHead {..}] <- mapMaybe (getGHead . gov'list . fst) $ getOutputDatumsWithTx @GovDatum ctx = + -- govLHead'feeRate + -- | otherwise = traceError' "Expecting excatly one gov head" + -- where + -- getGHead HeadLList {..} = Just _head'info + -- getGHead _ = Nothing + + getHead h = case h of + HeadDatum h' -> Just h' + _ -> Nothing + + getNode n = case n of + NodeDatum n' -> Just n' + _ -> Nothing + + subtractFee price = price - calcFee price + + calcFee price = round (fromInteger price * feeRate) + + sort2On f (x, y) = if f x < f y then (x, y) else (y, x) + + fst3 (x, _, _) = x + + -- containsNft !v = valueOf v nftCurr (nftTokenName datum') == 1 + + !getAda = flip assetClassValueOf $ assetClass adaSymbol adaToken + + -- Check if the Person is being reimbursed accordingly, with the help of 2 + -- getter functions. Helper function. + correctPayment node !userIdGetter !shareCalcFn !bid = personGetsAda >= personWantsAda + where + personId = getUserId . userIdGetter $ node + share = info'share . node'information $ node + personGetsAda = getAda $ valuePaidTo info (unPaymentPubKeyHash personId) + personWantsAda = subtractFee . getAda $ shareCalcFn bid share + + ownerIsAuthor = + (info'owner . node'information $ oldNode) == (info'author . node'information $ oldNode) + + -- withAuctionState node f = maybe (traceError "Auction state expected") f (mauctionState node) + + -- newDatum = case getOutputDatums ctx of + -- [x] -> x + -- [] -> error () + -- _ -> error () + + -- newNodeInfo :: InformationNft + -- newNodeInfo = + -- case newDatum of + -- HeadDatum _ -> error () -- traceError "nextNodeInfo: expected NodeDatum, got HeadDatum instead" + -- NodeDatum listNode -> node'information listNode + + -- Check if Datum id matches NFT id in UTXO + checkTxDatumMatch nodeDatum tx = + let cur = app'symbol . act'symbol $ act + tn = TokenName . toSpooky . nftId'contentHash . info'id . node'information $ nodeDatum + in valueOf (txOutValue tx) cur tn == 1 + + fromJust !x = fromMaybe (traceError' "fromJust") x + + ------------------------------------------------------------------------------ + -- Checks + extractCurr c = + mconcat + . fmap (uncurry3 singleton) + . filter ((== c) . fst3) + . flattenValue + + -- Check whether there's auction in progress and disallow buy/setprice actions. + -- noAuctionInProgress :: NftListNode -> Bool + -- noAuctionInProgress = isNothing . mauctionState + + -- auctionBidHighEnough :: NftListNode -> Integer -> Bool + -- auctionBidHighEnough node amount = + -- withAuctionState node $ \auctionState -> + -- case as'highestBid auctionState of + -- Nothing -> amount >= as'minBid auctionState + -- Just highestBid -> amount > ab'bid highestBid + + -- correctAuctionBidSlotInterval :: NftListNode -> Bool + -- correctAuctionBidSlotInterval node = + -- withAuctionState node $ \auctionState -> + -- to (as'deadline auctionState) `contains` txInfoValidRange info + + -- auctionDeadlineReached :: NftListNode -> Bool + -- auctionDeadlineReached node = + -- withAuctionState node $ \auctionState -> + -- from (as'deadline auctionState) `contains` txInfoValidRange info + + -- auctionCorrectPayment :: NftListNode -> (Integer -> Bool) -> Bool + -- auctionCorrectPayment node correctPaymentCheck = + -- withAuctionState node $ \auctionState -> + -- case as'highestBid auctionState of + -- Nothing -> True + -- Just (AuctionBid bid _bidder) -> + -- correctPaymentCheck bid + + -- auctionCorrectPaymentOwner :: NftListNode -> Bool + -- auctionCorrectPaymentOwner node = auctionCorrectPayment node (correctPaymentOwner node) + + -- auctionCorrectPaymentAuthor :: NftListNode -> Bool + -- auctionCorrectPaymentAuthor node = auctionCorrectPayment node (correctPaymentAuthor node) + + -- auctionCorrectPaymentOnlyAuthor :: NftListNode -> Bool + -- auctionCorrectPaymentOnlyAuthor node = + -- withAuctionState node $ \auctionState -> + -- case as'highestBid auctionState of + -- Nothing -> True + -- Just (AuctionBid bid _) -> + -- correctPaymentOnlyAuthor node bid + + -- correctBidRefund :: NftListNode -> Bool + -- correctBidRefund node = + -- withAuctionState node $ \auctionState -> + -- case as'highestBid auctionState of + -- Nothing -> True + -- Just (AuctionBid bid bidder) -> + -- valuePaidTo info (getUserId bidder) == lovelaceValueOf bid + + -- correctInputValue :: NftListNode -> Bool + -- correctInputValue node = + -- case findOwnInput ctx of + -- Nothing -> traceError "findOwnInput: Nothing" + -- Just (TxInInfo _ out) -> + -- case mauctionState node of + -- Nothing -> traceError "mauctionState: Nothing" + -- Just as -> case as'highestBid as of + -- Nothing -> tokenValue == txOutValue out + -- Just hb -> txOutValue out == (tokenValue <> lovelaceValueOf (ab'bid hb)) + + -- auctionBidValueSupplied :: Integer -> Bool + -- auctionBidValueSupplied redeemerBid = + -- case fmap snd . getOutputDatumsWithTx @DatumNft $ ctx of + -- [out] -> txOutValue out == tokenValue <> lovelaceValueOf redeemerBid + -- [] -> traceError "auctionBidValueSupplied: expected exactly one continuing output, got none" + -- _ -> traceError "auctionBidValueSupplied: expected exactly one continuing output, got several instead" + + -- auctionCorrectNewOwner :: NftListNode -> Bool + -- auctionCorrectNewOwner node = + -- withAuctionState node $ \auctionState -> + -- case as'highestBid auctionState of + -- Nothing -> True + -- Just (AuctionBid _ bidder) -> + -- bidder == newOwner + -- where + -- newOwner = info'owner newNodeInfo + + -- auctionConsistentCloseDatum :: NftListNode -> Bool + -- auctionConsistentCloseDatum node = + -- -- Checking that all fields remain the same except owner + -- info'id newNodeInfo == info'id nInfo' + -- && info'share newNodeInfo == info'share nInfo' + -- && info'author newNodeInfo == info'author nInfo' + -- && info'price newNodeInfo == info'price nInfo' + -- && checkOwner + -- where + -- nInfo' = nInfo node + + -- checkOwner = withAuctionState node $ \auctionState -> + -- case as'highestBid auctionState of + -- Nothing -> info'owner newNodeInfo == info'owner nInfo' + -- _ -> True + + -- auctionConsistentOpenDatum :: NftListNode -> Bool + -- auctionConsistentOpenDatum node = + -- -- Checking that all fields remain the same except auctionState + -- info'id newNodeInfo == info'id nInfo' + -- && info'share newNodeInfo == info'share nInfo' + -- && info'author newNodeInfo == info'author nInfo' + -- && info'owner newNodeInfo == info'owner nInfo' + -- where + -- nInfo' = nInfo node + + -- checkPriceIsNothing = isNothing . info'price $ newNodeInfo + + -- auctionConsistentDatum :: NftListNode -> Integer -> Bool + -- auctionConsistentDatum node redeemerBid = + -- let nInfo' = nInfo node + -- checkAuctionState = + -- case (info'auctionState newNodeInfo, info'auctionState nInfo') of + -- ( Just (AuctionState _ nextDeadline nextMinBid) + -- , Just (AuctionState _ deadline minBid) + -- ) -> + -- nextDeadline == deadline && nextMinBid == minBid + -- _ -> traceError "auctionConsistentDatum (checkAauctionState): expected auction state" + + -- checkHighestBid = + -- case (info'auctionState newNodeInfo, info'auctionState nInfo') of + -- ( Just (AuctionState (Just (AuctionBid nextBid _)) _ _) + -- , Just (AuctionState (Just (AuctionBid bid _)) _ _) + -- ) -> + -- nextBid > bid && nextBid == redeemerBid + -- ( Just (AuctionState (Just (AuctionBid nextBid _)) _ _) + -- , Just (AuctionState Nothing _ minBid) + -- ) -> + -- nextBid >= minBid && nextBid == redeemerBid + -- _ -> traceError "auctionConsistentDatum (checkHighestBid): expected auction state" + -- in info'id newNodeInfo == info'id nInfo' + -- && info'share newNodeInfo == info'share nInfo' + -- && info'author newNodeInfo == info'author nInfo' + -- && info'owner newNodeInfo == info'owner nInfo' + -- && info'price newNodeInfo == info'price nInfo' + -- && checkAuctionState + -- && checkHighestBid + + -- Check if changed only owner and price + consistentDatumBuy node = + let validAuthor = info'author . node'information $ node + validPrice = info'price . node'information $ node + validInfo = (node'information oldNode) {info'author' = toSpooky validAuthor, info'price' = toSpooky validPrice} + validNode = oldNode {node'information' = toSpooky validInfo} + in validNode == node + + -- Check if nft is for sale (price is not Nothing) + nftForSale = isJust . info'price . node'information $ oldNode + + -- Check if author of NFT receives share + correctPaymentAuthor node = correctPayment node (info'author . node'information) calculateAuthorShare + + -- Check if owner of NFT receives share + correctPaymentOwner node = correctPayment node (info'owner . node'information) calculateOwnerShare + + -- Check if author of NFT receives share when is also owner + correctPaymentOnlyAuthor node = correctPayment node (info'owner . node'information) (\v _ -> lovelaceValueOf v) + + -- Check if buy bid is higher or equal than price + bidHighEnough !bid = (fromJust . info'price . node'information $ oldNode) <= bid + + -- Check if the datum attached is also present in the set price transaction. + correctDatumSetPrice node = (== (info'id . nInfo) node) . info'id . node'information $ oldNode + + -- Check if only thing changed in nodes is price + consistentDatumSetPrice node = + let validPrice = info'price . node'information $ node + validInfo = (node'information oldNode) {info'price' = toSpooky validPrice} + validNode = oldNode {node'information' = toSpooky validInfo} + in validNode == node + + -- checkIsNotOnAuction = isNothing . info'auctionState . node'information + + -- Check if the price of NFT is changed by the owner of NFT + signedByOwner node = + case txInfoSignatories $ scriptContextTxInfo ctx of + [pkh] -> pkh == unPaymentPubKeyHash (getUserId (info'owner $ node'information node)) + _ -> False + + -- Check If No new token is minted. + !noMint = all ((nftCurr /=) . fst3) . flattenValue $ minted + where + minted = txInfoMint . scriptContextTxInfo $ ctx + + -- Check if exactly two Datums are attached to Mint output, and ids matches + checkMissMatchDatumMint = case getOutputDatumsWithTx @DatumNft ctx of + [x, y] -> case sort2On fst (x, y) of + ((HeadDatum _, _), (NodeDatum datum2, tx2)) -> checkTxDatumMatch datum2 tx2 + ((NodeDatum datum1, tx1), (NodeDatum datum2, tx2)) -> + checkTxDatumMatch datum1 tx1 && checkTxDatumMatch datum2 tx2 + _ -> False + _ -> False + + -- Check if exactly one Node is attached to outputs, and ids matches + checkMissMatchDatum = case filter (isNode . fst) . getOutputDatumsWithTx @DatumNft $ ctx of + [(NodeDatum datum, tx)] -> checkTxDatumMatch datum tx + _ -> False + + -- Check if exactly one Node is attached to inputs, and ids matches + onlyOneNodeAttached = case filter (isNode . fst) . getInputDatumsWithTx @DatumNft $ ctx of + [] -> traceError' "onlyOneNodeAttached: None provided" + [(NodeDatum datum, tx)] -> checkTxDatumMatch datum tx + _ -> traceError' "onlyOneNodeAttached: More provided" + + isNode (NodeDatum _) = True + isNode _ = False + + -- Check if all tokens from input and mint are returned + checkTokenReturned = + let addr = appInstance'Address . node'appInstance $ oldNode + inNfts = + extractCurr nftCurr + . (\tx -> mconcat (txOutValue . txInInfoResolved <$> txInfoInputs tx) <> txInfoMint tx) + . scriptContextTxInfo + $ ctx + outNfts = + extractCurr nftCurr + . mconcat + . fmap txOutValue + . filter ((addr ==) . txOutAddress) + . txInfoOutputs + . scriptContextTxInfo + $ ctx + in inNfts == outNfts + +{-# INLINEABLE catMaybes' #-} +catMaybes' :: [Maybe a] -> [a] +catMaybes' = catMaybes + +{-# INLINEABLE priceNotNegative #-} +priceNotNegative :: Maybe Integer -> Bool +priceNotNegative = maybe True (>= 0) + +data NftTrade +instance ValidatorTypes NftTrade where + type DatumType NftTrade = DatumNft + type RedeemerType NftTrade = UserAct + +{-# INLINEABLE txPolicy #-} +txPolicy :: UniqueToken -> TypedValidator Any +txPolicy x = unsafeMkTypedValidator v + where + v = + mkValidatorScript + ($$(PlutusTx.compile [||wrap||]) `PlutusTx.applyCode` ($$(PlutusTx.compile [||mkTxPolicy||]) `PlutusTx.applyCode` PlutusTx.liftCode x)) + validatorUntyped = wrap (mkTxPolicy x) + wrap = myWrapValidator @DatumNft @UserAct @ScriptContext + +{-# INLINEABLE txValHash #-} +txValHash :: UniqueToken -> ValidatorHash +txValHash = validatorHash . txPolicy + +{-# INLINEABLE txScrAddress #-} +txScrAddress :: UniqueToken -> Address +txScrAddress = toSpookyAddress . validatorAddress . txPolicy + +{-# INLINEABLE curSymbol #-} + +-- | Calculate the currency symbol of the NFT. +curSymbol :: NftAppInstance -> CurrencySymbol +curSymbol = toSpookyCurrencySymbol . scriptCurrencySymbol . mintPolicy + +{-# INLINEABLE nftCurrency #-} + +-- | Calculate the NFT `CurrencySymbol` from NftId. +nftCurrency :: DatumNft -> CurrencySymbol +nftCurrency = \case + HeadDatum x -> curSymbol $ head'appInstance x + NodeDatum x -> curSymbol $ node'appInstance x + +{-# INLINEABLE nftAsset #-} + +-- | Calculate the NFT `AssetClass` from Datum. +nftAsset :: DatumNft -> AssetClass +nftAsset datum = + assetClass + (nftCurrency datum) + (nftTokenName datum) + +{-# INLINEABLE calculateShares #-} + +{- | Returns the amount each party should be paid given the number of shares + retained by author. +-} +calculateShares :: Integer -> Rational -> (Value, Value) +calculateShares bid authorShare = (toOwner, toAuthor) + where + authorPart = round $ fromInteger bid * authorShare + toAuthor = lovelaceValueOf authorPart + toOwner = lovelaceValueOf $ bid - authorPart + +{-# INLINEABLE calculateOwnerShare #-} + +-- | Returns the calculated value of shares. +calculateOwnerShare :: Integer -> Rational -> Value +calculateOwnerShare x y = fst $ calculateShares x y + +{-# INLINEABLE calculateAuthorShare #-} + +-- | Returns the calculated value of shares. +calculateAuthorShare :: Integer -> Rational -> Value +calculateAuthorShare x y = snd $ calculateShares x y + +{-# INLINEABLE getInputDatums #-} + +-- | Returns datums attached to inputs of transaction +getInputDatums :: PlutusTx.FromData a => ScriptContext -> [a] +getInputDatums = fmap fst . getInputDatumsWithTx + +{-# INLINEABLE getInputDatumsWithTx #-} + +-- | Returns datums and corresponding UTXOs attached to inputs of transaction +getInputDatumsWithTx :: PlutusTx.FromData a => ScriptContext -> [(a, TxOut)] +getInputDatumsWithTx ctx = + mapMaybe (\(datum, tx) -> (,) <$> (PlutusTx.fromBuiltinData . getDatum $ datum) <*> pure tx) + . mapMaybe (\(hash, tx) -> (,) <$> findDatum hash (scriptContextTxInfo ctx) <*> pure tx) + . mapMaybe ((\tx -> (,) <$> txOutDatumHash tx <*> pure tx) . txInInfoResolved) + . txInfoInputs + . scriptContextTxInfo + $ ctx + +{-# INLINEABLE getOutputDatums #-} + +-- | Returns datums attached to outputs of transaction +getOutputDatums :: PlutusTx.FromData a => ScriptContext -> [a] +getOutputDatums = fmap fst . getOutputDatumsWithTx + +{-# INLINEABLE getOutputDatumsWithTx #-} + +-- | Returns datums and coresponding UTXOs attached to outputs of transaction +getOutputDatumsWithTx :: PlutusTx.FromData a => ScriptContext -> [(a, TxOut)] +getOutputDatumsWithTx ctx = + mapMaybe (\(datum, tx) -> (,) <$> (PlutusTx.fromBuiltinData . getDatum $ datum) <*> pure tx) + . mapMaybe (\(hash, tx) -> (,) <$> findDatum hash (scriptContextTxInfo ctx) <*> pure tx) + . mapMaybe (\tx -> (,) <$> txOutDatumHash tx <*> pure tx) + . txInfoOutputs + . scriptContextTxInfo + $ ctx + +-- Switch definitons for meaningful errors during development + +{-# INLINEABLE traceIfFalse' #-} +traceIfFalse' :: BuiltinString -> Bool -> Bool +traceIfFalse' _ x = x + +-- traceIfFalse' = traceIfFalse + +{-# INLINEABLE traceError' #-} +traceError' :: BuiltinString -> a +traceError' _ = error () + +-- traceError' = traceError + +{-# INLINEABLE myWrapValidator #-} +myWrapValidator :: + forall d r p. + (PlutusTx.UnsafeFromData d, PlutusTx.UnsafeFromData r, PlutusTx.UnsafeFromData p) => + (d -> r -> p -> Bool) -> + BuiltinData -> + BuiltinData -> + BuiltinData -> + () +myWrapValidator f d r p = check (f (PlutusTx.unsafeFromBuiltinData d) (PlutusTx.unsafeFromBuiltinData r) (PlutusTx.unsafeFromBuiltinData p)) + +{-# INLINEABLE myWrapMintingPolicy #-} +myWrapMintingPolicy :: + PlutusTx.UnsafeFromData r => + (r -> ScriptContext -> Bool) -> + WrappedMintingPolicyType +-- We can use unsafeFromBuiltinData here as we would fail immediately anyway if parsing failed +myWrapMintingPolicy f r p = check $ f (PlutusTx.unsafeFromBuiltinData r) (PlutusTx.unsafeFromBuiltinData p) diff --git a/mlabs/src/Mlabs/NftStateMachine/Contract.hs b/mlabs/src/Mlabs/NftStateMachine/Contract.hs new file mode 100644 index 000000000..de77527d6 --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Contract.hs @@ -0,0 +1,9 @@ +-- | Re-export module +module Mlabs.NftStateMachine.Contract ( + module X, +) where + +import Mlabs.NftStateMachine.Contract.Api as X +import Mlabs.NftStateMachine.Contract.Forge as X +import Mlabs.NftStateMachine.Contract.Server as X +import Mlabs.NftStateMachine.Contract.StateMachine as X diff --git a/mlabs/src/Mlabs/NftStateMachine/Contract/Api.hs b/mlabs/src/Mlabs/NftStateMachine/Contract/Api.hs new file mode 100644 index 000000000..fa04cda49 --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Contract/Api.hs @@ -0,0 +1,90 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Contract API for Lendex application +module Mlabs.NftStateMachine.Contract.Api ( + Buy (..), + SetPrice (..), + StartParams (..), + UserSchema, + AuthorSchema, + IsUserAct (..), +) where + +import GHC.Generics (Generic) +import Playground.Contract (FromJSON, ToJSON, ToSchema) +import Plutus.Contract (type (.\/)) +import PlutusTx.Prelude (BuiltinByteString, Integer, Maybe, Rational) +import Prelude qualified as Hask (Eq, Show) + +import Mlabs.NftStateMachine.Logic.Types (UserAct (BuyAct, SetPriceAct)) +import Mlabs.Plutus.Contract (Call, IsEndpoint (..)) + +---------------------------------------------------------------------- +-- NFT endpoints + +-- user endpoints + +-- | User buys NFT +data Buy = Buy + { buy'price :: Integer + , buy'newPrice :: Maybe Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +-- | User sets new price for NFT +newtype SetPrice = SetPrice + { setPrice'newPrice :: Maybe Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +-- author endpoints + +-- | Parameters to init NFT +data StartParams = StartParams + { -- | NFT content + sp'content :: BuiltinByteString + , -- | author share [0, 1] on reselling of the NFT + sp'share :: Rational + , -- | current price of NFT, if it's nothing then nobody can buy it. + sp'price :: Maybe Integer + } + deriving stock (Hask.Show, Generic) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +---------------------------------------------------------------------- +-- schemas + +-- | User schema. Owner can set the price and the buyer can try to buy. +type UserSchema = + Call Buy + .\/ Call SetPrice + +-- | Schema for the author of NFT +type AuthorSchema = + Call StartParams + +---------------------------------------------------------------------- +-- classes + +class IsUserAct a where + toUserAct :: a -> UserAct + +instance IsUserAct Buy where toUserAct Buy {..} = BuyAct buy'price buy'newPrice +instance IsUserAct SetPrice where toUserAct SetPrice {..} = SetPriceAct setPrice'newPrice + +instance IsEndpoint Buy where + type EndpointSymbol Buy = "buy-nft" + +instance IsEndpoint SetPrice where + type EndpointSymbol SetPrice = "set-price-for-nft" + +instance IsEndpoint StartParams where + type EndpointSymbol StartParams = "start-nft" diff --git a/mlabs/src/Mlabs/NftStateMachine/Contract/Emulator/Client.hs b/mlabs/src/Mlabs/NftStateMachine/Contract/Emulator/Client.hs new file mode 100644 index 000000000..66869a7f1 --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Contract/Emulator/Client.hs @@ -0,0 +1,39 @@ +-- | Client functions to test contracts in EmulatorTrace monad. +module Mlabs.NftStateMachine.Contract.Emulator.Client ( + callUserAct, + callStartNft, +) where + +import Prelude + +import Data.Functor (void) +import Data.Monoid (Last (..)) +import Plutus.Trace.Emulator (EmulatorRuntimeError (..), EmulatorTrace, activateContractWallet, observableState, throwError, waitNSlots) +import Wallet.Emulator (Wallet) + +import Mlabs.NftStateMachine.Contract.Api (Buy (..), SetPrice (..), StartParams) +import Mlabs.NftStateMachine.Contract.Server (authorEndpoints, userEndpoints) +import Mlabs.NftStateMachine.Logic.Types qualified as Types +import Mlabs.Plutus.Contract (callEndpoint') + +--------------------------------------------------------- +-- call endpoints (for debug and testing) + +-- | Calls user act +callUserAct :: Types.NftId -> Wallet -> Types.UserAct -> EmulatorTrace () +callUserAct nid wal act = do + hdl <- activateContractWallet wal (userEndpoints nid) + void $ case act of + Types.BuyAct {..} -> callEndpoint' hdl (Buy act'price act'newPrice) + Types.SetPriceAct {..} -> callEndpoint' hdl (SetPrice act'newPrice) + +-- | Calls initialisation of state for Nft pool +callStartNft :: Wallet -> StartParams -> EmulatorTrace Types.NftId +callStartNft wal sp = do + hdl <- activateContractWallet wal authorEndpoints + void $ callEndpoint' hdl sp + void $ waitNSlots 10 + Last nid <- observableState hdl + maybe err pure nid + where + err = throwError $ GenericError "No NFT started in emulator" diff --git a/mlabs/src/Mlabs/NftStateMachine/Contract/Forge.hs b/mlabs/src/Mlabs/NftStateMachine/Contract/Forge.hs new file mode 100644 index 000000000..7cd97692b --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Contract/Forge.hs @@ -0,0 +1,71 @@ +{-# LANGUAGE BangPatterns #-} + +-- | Validation of forge for NFTs +module Mlabs.NftStateMachine.Contract.Forge ( + currencyPolicy, + currencySymbol, +) where + +import PlutusTx.Prelude + +import Ledger (Address, CurrencySymbol) +import Ledger.Contexts qualified as Contexts +import Ledger.Typed.Scripts (MintingPolicy) +import Ledger.Typed.Scripts qualified as Scripts +import Plutus.V1.Ledger.Scripts qualified as Scripts +import Plutus.V1.Ledger.Value qualified as Value +import PlutusTx qualified + +import Mlabs.NftStateMachine.Logic.Types (NftId (NftId)) + +{-# INLINEABLE validate #-} + +{- | Validation of minting of NFT-token. We guarantee uniqueness of NFT + by make the script depend on spending of concrete TxOutRef in the list of inputs. + TxOutRef for the input is specified inside NftId value. + + Also we check that + + * user mints token that corresponds to the content of NFT (token name is hash of NFT content) + * user spends NFT token to the StateMachine script + + First argument is an address of NFT state machine script. We use it to check + that NFT coin was payed to script after minting. +-} +validate :: Address -> NftId -> BuiltinData -> Contexts.ScriptContext -> Bool +validate !stateAddr (NftId token !oref) _ !ctx = + traceIfFalse "UTXO not consumed" hasUtxo + && traceIfFalse "wrong amount minted" checkMintedAmount + && traceIfFalse "Does not pay to state" paysToState + where + !info = Contexts.scriptContextTxInfo ctx + + !hasUtxo = any (\inp -> Contexts.txInInfoOutRef inp == oref) $ Contexts.txInfoInputs info + + !checkMintedAmount = case Value.flattenValue (Contexts.txInfoMint info) of + [(cur, tn, !val)] -> Contexts.ownCurrencySymbol ctx == cur && token == tn && val == 1 + _ -> False + + !paysToState = any hasNftToken $ Contexts.txInfoOutputs info + + hasNftToken Contexts.TxOut {..} = + txOutAddress == stateAddr + && txOutValue == Value.singleton (Contexts.ownCurrencySymbol ctx) token 1 + +------------------------------------------------------------------------------- + +{- | Minting policy of NFT + First argument is an address of NFT state machine script. +-} +currencyPolicy :: Address -> NftId -> MintingPolicy +currencyPolicy stateAddr nid = + Scripts.mkMintingPolicyScript $ + $$(PlutusTx.compile [||\x y -> Scripts.wrapMintingPolicy (validate x y)||]) + `PlutusTx.applyCode` PlutusTx.liftCode stateAddr + `PlutusTx.applyCode` PlutusTx.liftCode nid + +{- | Currency symbol of NFT + First argument is an address of NFT state machine script. +-} +currencySymbol :: Address -> NftId -> CurrencySymbol +currencySymbol stateAddr nid = Contexts.scriptCurrencySymbol (currencyPolicy stateAddr nid) diff --git a/mlabs/src/Mlabs/NftStateMachine/Contract/Server.hs b/mlabs/src/Mlabs/NftStateMachine/Contract/Server.hs new file mode 100644 index 000000000..1b312ea61 --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Contract/Server.hs @@ -0,0 +1,102 @@ +module Mlabs.NftStateMachine.Contract.Server ( + -- * Contracts + UserContract, + AuthorContract, + + -- * Endpoints + userEndpoints, + authorEndpoints, + startNft, +) where + +import Prelude (String, (<>)) + +import Control.Lens (preview) +import Control.Monad (forever) +import Data.Map qualified as M +import Data.Monoid (Last (..)) +import Ledger (pubKeyHashAddress) +import Ledger.Constraints (mintingPolicy, mustIncludeDatum, mustMintValue, mustSpendPubKeyOutput) +import Ledger.Constraints qualified as Constraints +import Ledger.Tx (ciTxOutDatum) +import Mlabs.Data.List (firstJustRight) +import Mlabs.Emulator.Types (ownUserId) +import Mlabs.NftStateMachine.Contract.Api (AuthorSchema, Buy, IsUserAct, SetPrice, StartParams (..), UserSchema, toUserAct) +import Mlabs.NftStateMachine.Contract.StateMachine qualified as SM +import Mlabs.NftStateMachine.Logic.Types (Act (UserAct), NftId, initNft, toNftId) +import Mlabs.Plutus.Contract (getEndpoint, selectForever) +import Plutus.Contract (Contract, logError, ownPaymentPubKeyHash, tell, throwError, toContract, utxosAt) +import Plutus.V1.Ledger.Api (Datum) +import PlutusTx.Prelude hiding ((<>)) + +-- | NFT contract for the user +type UserContract a = Contract () UserSchema SM.NftError a + +-- | Contract for the author of NFT +type AuthorContract a = Contract (Last NftId) AuthorSchema SM.NftError a + +---------------------------------------------------------------- +-- endpoints + +-- | Endpoints for user +userEndpoints :: NftId -> UserContract () +userEndpoints nid = + selectForever + [ getEndpoint @Buy $ userAction nid + , getEndpoint @SetPrice $ userAction nid + ] + +-- | Endpoints for admin user +authorEndpoints :: AuthorContract () +authorEndpoints = forever startNft' + where + startNft' = toContract $ getEndpoint @StartParams $ startNft + +userAction :: IsUserAct a => NftId -> a -> UserContract () +userAction nid input = do + pkh <- ownPaymentPubKeyHash + act <- getUserAct input + inputDatum <- findInputStateDatum nid + let lookups = + mintingPolicy (SM.nftPolicy nid) + <> Constraints.ownPaymentPubKeyHash pkh + constraints = mustIncludeDatum inputDatum + SM.runStepWith nid act lookups constraints + +{- | Initialise NFt endpoint. + We save NftId to the contract writer. +-} +startNft :: StartParams -> AuthorContract () +startNft StartParams {..} = do + orefs <- M.keys <$> (utxosAt . (`pubKeyHashAddress` Nothing) =<< ownPaymentPubKeyHash) + case orefs of + [] -> logError @String "No UTXO found" + oref : _ -> do + let nftId = toNftId oref sp'content + val = SM.nftValue nftId + lookups = mintingPolicy $ SM.nftPolicy nftId + tx = + mustMintValue val + <> mustSpendPubKeyOutput oref + authorId <- ownUserId + SM.runInitialiseWith nftId (initNft oref authorId sp'content sp'share sp'price) val lookups tx + tell $ Last $ Just nftId + +---------------------------------------------------------------- + +-- | Converts endpoint inputs to logic actions +getUserAct :: IsUserAct a => a -> UserContract Act +getUserAct act = do + uid <- ownUserId + pure $ UserAct uid $ toUserAct act + +---------------------------------------------------------------- +-- utils + +-- | Finds Datum for NFT state machine script. +findInputStateDatum :: NftId -> UserContract Datum +findInputStateDatum nid = do + utxos <- utxosAt (SM.nftAddress nid) + maybe err pure $ firstJustRight (preview ciTxOutDatum . snd) $ M.toList utxos + where + err = throwError $ SM.toNftError "Can not find NFT app instance" diff --git a/mlabs/src/Mlabs/NftStateMachine/Contract/Simulator/Handler.hs b/mlabs/src/Mlabs/NftStateMachine/Contract/Simulator/Handler.hs new file mode 100644 index 000000000..ca1cc330e --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Contract/Simulator/Handler.hs @@ -0,0 +1,107 @@ +-- | Handlers for PAB simulator +module Mlabs.NftStateMachine.Contract.Simulator.Handler ( + Sim, + NftContracts (..), + runSimulator, +) where + +import Prelude + +-- FIXME +-- handler related imports commented out with `-- !` to disable compilation warnings +-- ! import Control.Monad.Freer ( +-- Eff, +-- Member, +-- interpret, +-- type (~>), +-- ) +-- ! import Control.Monad.Freer.Error (Error) +-- ! import Control.Monad.Freer.Extras.Log (LogMsg) +import Control.Monad.IO.Class (MonadIO (..)) +import Data.Aeson (FromJSON, ToJSON) +import Data.Functor (void) + +-- ! import Data.Monoid (Last) +import Data.OpenApi.Schema qualified as OpenApi + +-- ! import Data.Text (Text, pack) + +import GHC.Generics (Generic) +import Prettyprinter (Pretty (..), viaShow) + +-- ! import Plutus.Contract (Contract, mapError) +-- ! import Plutus.PAB.Effects.Contract (ContractEffect (..)) +import Plutus.PAB.Effects.Contract.Builtin (Builtin) + +-- ! import Plutus.PAB.Monitoring.PABLogMsg (PABMultiAgentMsg (..)) +import Plutus.PAB.Simulator ( + Simulation, + SimulatorEffectHandlers, + ) +import Plutus.PAB.Simulator qualified as Simulator + +-- ! import Plutus.PAB.Types (PABError (..)) +import Plutus.PAB.Webserver.Server qualified as PAB.Server + +import Mlabs.NftStateMachine.Contract.Api qualified as Nft + +-- ! import Mlabs.NftStateMachine.Contract.Server qualified as Nft +import Mlabs.NftStateMachine.Logic.Types (NftId) + +-- | Shortcut for Simulator monad for NFT case +type Sim a = Simulation (Builtin NftContracts) a + +-- | NFT schemas +data NftContracts + = -- | author can start NFT and provide NftId + StartNft + | -- | we read NftId and instantiate schema for the user actions + User NftId + deriving stock (Show, Generic) + deriving anyclass (FromJSON, ToJSON, OpenApi.ToSchema) + +instance Pretty NftContracts where + pretty = viaShow + +-- FIXME +-- related imports commented out to disable compilation warnings +-- handleNftContracts :: +-- ( Member (Error PABError) effs , +-- Member (LogMsg (PABMultiAgentMsg (Builtin NftContracts))) effs +-- ) => +-- Nft.StartParams -> +-- ContractEffect (Builtin NftContracts) ~> Eff effs +-- handleNftContracts sp = +-- Builtin.handleBuiltin getSchema getContract +-- where +-- getSchema = \case +-- StartNft -> Builtin.endpointsToSchemas @Nft.AuthorSchema +-- User _ -> Builtin.endpointsToSchemas @Nft.UserSchema +-- getContract = \case +-- StartNft -> SomeBuiltin (startNftContract sp) +-- User nid -> SomeBuiltin (Nft.userEndpoints nid) + +-- FIXME +handlers :: Nft.StartParams -> SimulatorEffectHandlers (Builtin NftContracts) +handlers = error "Fix required after Plutus update" + +-- handlers sp = +-- Simulator.mkSimulatorHandlers @(Builtin NftContracts) def def $ +-- interpret (handleNftContracts sp) + +-- FIXME +-- startNftContract :: Nft.StartParams -> Contract (Last NftId) Nft.AuthorSchema Text () +-- startNftContract startParams = mapError (pack . show) $ Nft.startNft startParams + +-- | Runs simulator for NFT +runSimulator :: Nft.StartParams -> Sim () -> IO () +runSimulator sp = withSimulator (handlers sp) + +withSimulator :: Simulator.SimulatorEffectHandlers (Builtin NftContracts) -> Simulation (Builtin NftContracts) () -> IO () +withSimulator hs act = void $ + Simulator.runSimulationWith hs $ do + Simulator.logString @(Builtin NftContracts) "Starting PAB webserver. Press enter to exit." + shutdown <- PAB.Server.startServerDebug + void act + void $ liftIO getLine + shutdown diff --git a/mlabs/src/Mlabs/NftStateMachine/Contract/StateMachine.hs b/mlabs/src/Mlabs/NftStateMachine/Contract/StateMachine.hs new file mode 100644 index 000000000..2b3c2d273 --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Contract/StateMachine.hs @@ -0,0 +1,161 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE UndecidableInstances #-} + +module Mlabs.NftStateMachine.Contract.StateMachine ( + NftMachine, + NftMachineClient, + NftError, + toNftError, + nftAddress, + nftPolicy, + nftValue, + runStepWith, + runInitialiseWith, + scriptInstance, +) where + +import PlutusTx.Prelude hiding (Applicative (..), Monoid (..), Semigroup (..), check) +import Prelude qualified as Hask (String) + +import Control.Monad.State.Strict (runStateT) +import Data.Functor (void) +import Data.String (fromString) +import Ledger (Address, MintingPolicy, ValidatorHash, scriptHashAddress) +import Ledger.Constraints (ScriptLookups, TxConstraints, mustBeSignedBy) +import Ledger.Typed.Scripts qualified as Scripts +import Plutus.Contract (Contract) +import Plutus.Contract.StateMachine qualified as SM +import Plutus.V1.Ledger.Value (AssetClass (..), CurrencySymbol, Value, assetClassValue) +import PlutusTx qualified +import PlutusTx.Prelude qualified as Plutus + +import Mlabs.Emulator.Blockchain (toConstraints, updateRespValue) +import Mlabs.Emulator.Types (UserId (..)) +import Mlabs.NftStateMachine.Contract.Forge qualified as Forge +import Mlabs.NftStateMachine.Logic.React (react) +import Mlabs.NftStateMachine.Logic.Types (Act (UserAct), Nft (nft'id), NftId (nftId'token)) + +type NftMachine = SM.StateMachine Nft Act +type NftMachineClient = SM.StateMachineClient Nft Act + +-- | NFT errors +type NftError = SM.SMContractError + +toNftError :: Hask.String -> NftError +toNftError = SM.SMCContractError . fromString + +{-# INLINEABLE machine #-} + +-- | State machine definition +machine :: NftId -> NftMachine +machine !nftId = SM.mkStateMachine Nothing (transition nftId) isFinal + where + !isFinal = const False + +{-# INLINEABLE mkValidator #-} + +-- | State machine validator +mkValidator :: NftId -> Scripts.ValidatorType NftMachine +mkValidator !nftId = SM.mkValidator (machine nftId) + +-- | State machine client +client :: NftId -> NftMachineClient +client nftId = SM.mkStateMachineClient $ SM.StateMachineInstance (machine nftId) (scriptInstance nftId) + +-- | NFT validator hash +nftValidatorHash :: NftId -> ValidatorHash +nftValidatorHash nftId = Scripts.validatorHash (scriptInstance nftId) + +-- | NFT script address +nftAddress :: NftId -> Address +nftAddress nftId = scriptHashAddress (nftValidatorHash nftId) + +-- | NFT script instance +scriptInstance :: NftId -> Scripts.TypedValidator NftMachine +scriptInstance nftId = + Scripts.mkTypedValidator @NftMachine + ( $$(PlutusTx.compile [||mkValidator||]) + `PlutusTx.applyCode` PlutusTx.liftCode nftId + ) + $$(PlutusTx.compile [||wrap||]) + where + wrap = Scripts.wrapValidator + +{-# INLINEABLE transition #-} + +-- | State transitions for NFT +transition :: + NftId -> + SM.State Nft -> + Act -> + Maybe (SM.TxConstraints SM.Void SM.Void, SM.State Nft) +transition !nftId SM.State {stateData = !oldData, stateValue = !oldValue} !input + | idIsValid = + case runStateT (react input) oldData of + Left _err -> Nothing + Right (!resps, !newData) -> + Just + ( foldMap toConstraints resps Plutus.<> ctxConstraints + , SM.State + { stateData = newData + , stateValue = updateRespValue resps oldValue + } + ) + | otherwise = Nothing + where + !idIsValid = nftId == nft'id oldData + + -- we check that user indeed signed the transaction with his own key + !ctxConstraints = maybe Plutus.mempty mustBeSignedBy userId + + !userId = case input of + UserAct (UserId uid) _ -> Just uid + _ -> Nothing + +----------------------------------------------------------------------- +-- NFT forge policy + +-- | NFT monetary policy +nftPolicy :: NftId -> MintingPolicy +nftPolicy nid = Forge.currencyPolicy (nftAddress nid) nid + +-- | NFT currency symbol +nftSymbol :: NftId -> CurrencySymbol +nftSymbol nid = Forge.currencySymbol (nftAddress nid) nid + +-- | NFT coin (AssetClass) +nftCoin :: NftId -> AssetClass +nftCoin nid = AssetClass (nftSymbol nid, nftId'token nid) + +-- | Single value of NFT coin. We check that there is only one NFT-coin can be minted. +nftValue :: NftId -> Value +nftValue nid = assetClassValue (nftCoin nid) 1 + +------------------------------------------------------------------------ + +runStepWith :: + forall w e schema. + SM.AsSMContractError e => + NftId -> + Act -> + ScriptLookups NftMachine -> + TxConstraints (Scripts.RedeemerType NftMachine) (Scripts.DatumType NftMachine) -> + Contract w schema e () +runStepWith nid act lookups constraints = void $ SM.runStepWith lookups constraints (client nid) act + +runInitialiseWith :: + SM.AsSMContractError e => + NftId -> + Nft -> + Value -> + ScriptLookups NftMachine -> + TxConstraints (Scripts.RedeemerType NftMachine) (Scripts.DatumType NftMachine) -> + Contract w schema e () +runInitialiseWith nftId nft val lookups tx = void $ SM.runInitialiseWith lookups tx (client nftId) nft val diff --git a/mlabs/src/Mlabs/NftStateMachine/Logic/App.hs b/mlabs/src/Mlabs/NftStateMachine/Logic/App.hs new file mode 100644 index 000000000..b335776d1 --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Logic/App.hs @@ -0,0 +1,90 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | Application for testing NFT logic. +module Mlabs.NftStateMachine.Logic.App ( + NftApp, + runNftApp, + AppCfg (..), + defaultAppCfg, + --- * Script + Script, + buy, + setPrice, +) where + +import PlutusTx.Prelude +import Prelude qualified as Hask (uncurry) + +import Data.Map.Strict qualified as M +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash)) +import Playground.Contract (TxOutRef (..)) +import Plutus.V1.Ledger.Crypto (PubKeyHash (..)) +import Plutus.V1.Ledger.TxId (TxId (TxId)) + +import Mlabs.Emulator.App (App (..), runApp) +import Mlabs.Emulator.Blockchain (BchState (BchState), BchWallet (..), defaultBchWallet) +import Mlabs.Emulator.Script qualified as S +import Mlabs.Emulator.Types (UserId (..), adaCoin) +import Mlabs.NftStateMachine.Logic.React (react) +import Mlabs.NftStateMachine.Logic.Types (Act (..), Nft, UserAct (BuyAct, SetPriceAct), initNft) +import PlutusTx.Ratio qualified as R + +-- | NFT test emulator. We use it test the logic. +type NftApp = App Nft Act + +-- | Config for NFT test emulator +data AppCfg = AppCfg + { -- | state of blockchain + appCfg'users :: [(UserId, BchWallet)] + , appCfg'nftInRef :: TxOutRef + , -- | nft content + appCfg'nftData :: BuiltinByteString + , -- | author of nft + appCfg'nftAuthor :: UserId + } + +-- | Run test emulator for NFT app. +runNftApp :: AppCfg -> Script -> NftApp +runNftApp cfg = runApp react (initApp cfg) + +-- | Initialise NFT application. +initApp :: AppCfg -> NftApp +initApp AppCfg {..} = + App + { app'st = initNft appCfg'nftInRef appCfg'nftAuthor appCfg'nftData (R.reduce 1 10) Nothing + , app'log = [] + , app'wallets = BchState $ M.fromList $ (Self, defaultBchWallet) : appCfg'users + } + +{- | Default application. + It allocates three users each of them has 1000 ada coins. + The first user is author and the owner of NFT. NFT is locked with no price. +-} +defaultAppCfg :: AppCfg +defaultAppCfg = AppCfg users dummyOutRef "mona-lisa" (fst . head $ users) + where + dummyOutRef = TxOutRef (TxId "") 0 + + userNames = ["1", "2", "3"] + + users = fmap (\userName -> (UserId (PaymentPubKeyHash (PubKeyHash userName)), wal (adaCoin, 1000))) userNames + wal cs = BchWallet $ Hask.uncurry M.singleton cs + +------------------------------------------------------- +-- script endpoints + +type Script = S.Script Act + +-- | User buys NFTs +buy :: UserId -> Integer -> Maybe Integer -> Script +buy uid price newPrice = S.putAct $ UserAct uid (BuyAct price newPrice) + +-- | Set price of NFT +setPrice :: UserId -> Maybe Integer -> Script +setPrice uid newPrice = S.putAct $ UserAct uid (SetPriceAct newPrice) diff --git a/mlabs/src/Mlabs/NftStateMachine/Logic/React.hs b/mlabs/src/Mlabs/NftStateMachine/Logic/React.hs new file mode 100644 index 000000000..d19921e98 --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Logic/React.hs @@ -0,0 +1,73 @@ +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-specialize #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fobject-code #-} + +-- | Transition function for NFTs +module Mlabs.NftStateMachine.Logic.React (react, checkInputs) where + +import Control.Monad.State.Strict (gets, modify') + +import PlutusTx.Prelude + +import Mlabs.Control.Check (isPositive) +import Mlabs.Emulator.Blockchain (Resp (Move)) +import Mlabs.Lending.Logic.Types (adaCoin) +import Mlabs.NftStateMachine.Logic.State (St, getAuthorShare, isOwner, isRightPrice) +import Mlabs.NftStateMachine.Logic.Types ( + Act (..), + Nft (nft'author, nft'owner, nft'price), + UserAct (BuyAct, SetPriceAct), + ) + +{-# INLINEABLE react #-} + +-- | State transitions for NFT contract logic. +react :: Act -> St [Resp] +react inp = do + checkInputs inp + case inp of + UserAct uid (BuyAct price newPrice) -> buyAct uid price newPrice + UserAct uid (SetPriceAct price) -> setPriceAct uid price + where + ----------------------------------------------- + -- buy + + buyAct uid price newPrice = do + isRightPrice price + authorShare <- getAuthorShare price + let total = authorShare + price + author <- gets nft'author + owner <- gets nft'owner + updateNftOnBuy + pure + [ Move uid adaCoin (negate total) + , Move owner adaCoin price + , Move author adaCoin authorShare + ] + where + updateNftOnBuy = + modify' $ \st -> + st + { nft'owner = uid + , nft'price = newPrice + } + + ----------------------------------------------- + -- set price + + setPriceAct uid price = do + isOwner uid + modify' $ \st -> st {nft'price = price} + pure [] + +{-# INLINEABLE checkInputs #-} + +-- | Check inputs for valid values. +checkInputs :: Act -> St () +checkInputs (UserAct _uid act) = case act of + BuyAct price newPrice -> do + isPositive "Buy price" price + mapM_ (isPositive "New price") newPrice + SetPriceAct price -> mapM_ (isPositive "Set price") price diff --git a/mlabs/src/Mlabs/NftStateMachine/Logic/State.hs b/mlabs/src/Mlabs/NftStateMachine/Logic/State.hs new file mode 100644 index 000000000..9a4dd2a69 --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Logic/State.hs @@ -0,0 +1,50 @@ +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-specialize #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fobject-code #-} + +-- | State transitions for NFT app +module Mlabs.NftStateMachine.Logic.State ( + St, + isOwner, + isRightPrice, + getAuthorShare, +) where + +import PlutusTx.Prelude + +import Mlabs.Control.Monad.State (PlutusState, gets, guardError) +import Mlabs.Lending.Logic.Types (UserId) +import Mlabs.NftStateMachine.Logic.Types (Nft (nft'owner, nft'price, nft'share)) +import PlutusTx.Ratio qualified as R + +-- | State update of NFT +type St = PlutusState Nft + +----------------------------------------------------------- +-- common functions + +{-# INLINEABLE isOwner #-} + +-- | Check if user is owner of NFT +isOwner :: UserId -> St () +isOwner uid = do + owner <- gets nft'owner + guardError "Not an owner" $ uid == owner + +{-# INLINEABLE isRightPrice #-} + +-- | Check if price is enough to buy NFT +isRightPrice :: Integer -> St () +isRightPrice inputPrice = do + isOk <- any (inputPrice >=) <$> gets nft'price + guardError "Price not enough" isOk + +{-# INLINEABLE getAuthorShare #-} + +-- | Get original author's share of the price of NFT +getAuthorShare :: Integer -> St Integer +getAuthorShare price = do + share <- gets nft'share + pure $ R.round $ R.fromInteger price * share diff --git a/mlabs/src/Mlabs/NftStateMachine/Logic/Types.hs b/mlabs/src/Mlabs/NftStateMachine/Logic/Types.hs new file mode 100644 index 000000000..0f3cfaa23 --- /dev/null +++ b/mlabs/src/Mlabs/NftStateMachine/Logic/Types.hs @@ -0,0 +1,120 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} +{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} +{-# OPTIONS_GHC -fno-specialize #-} +{-# OPTIONS_GHC -fno-strictness #-} +{-# OPTIONS_GHC -fobject-code #-} + +-- | Datatypes for NFT state machine. +module Mlabs.NftStateMachine.Logic.Types ( + Nft (..), + NftId (..), + initNft, + toNftId, + Act (..), + UserAct (..), +) where + +import PlutusTx.Prelude + +import Data.Aeson (FromJSON, ToJSON) +import Data.OpenApi.Schema qualified as OpenApi +import GHC.Generics (Generic) +import Playground.Contract (ToSchema, TxOutRef) +import Plutus.V1.Ledger.Value (TokenName (..)) +import PlutusTx qualified +import Prelude qualified as Hask (Eq, Show) + +import Mlabs.Emulator.Types (UserId (..)) + +-- | Data for NFTs +data Nft = Nft + { -- | token name, unique identifier for NFT + nft'id :: NftId + , -- | data (media, audio, photo, etc) + nft'data :: BuiltinByteString + , -- | share for the author on each sell + nft'share :: Rational + , -- | author + nft'author :: UserId + , -- | current owner + nft'owner :: UserId + , -- | price in ada, if it's nothing then nobody can buy + nft'price :: Maybe Integer + } + deriving stock (Hask.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +-- | Unique identifier of NFT. +data NftId = NftId + { -- | token name is identified by content of the NFT (it's hash of it) + nftId'token :: TokenName + , -- | TxOutRef that is used for minting of NFT, + -- with it we can guarantee uniqueness of NFT + nftId'outRef :: TxOutRef + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON, ToSchema, OpenApi.ToSchema) + +instance Eq NftId where + {-# INLINEABLE (==) #-} + (==) (NftId tok1 oref1) (NftId tok2 oref2) = + tok1 == tok2 && oref1 == oref2 + +{-# INLINEABLE initNft #-} + +-- | Initialise NFT +initNft :: TxOutRef -> UserId -> BuiltinByteString -> Rational -> Maybe Integer -> Nft +initNft nftInRef author content share mPrice = + Nft + { nft'id = toNftId nftInRef content + , nft'data = content + , nft'share = share + , nft'author = author + , nft'owner = author + , nft'price = mPrice + } + +{-# INLINEABLE toNftId #-} + +-- | Calculate NFT identifier from it's content (data). +toNftId :: TxOutRef -> BuiltinByteString -> NftId +toNftId oref content = NftId (TokenName $ sha2_256 content) oref + +-- | Actions with NFTs with UserId. +data Act = UserAct UserId UserAct + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +-- | Actions with NFTs +data UserAct + = -- | Buy NFT and set new price + BuyAct + { -- | price to buy + act'price :: Integer + , -- | new price for NFT (Nothing locks NFT) + act'newPrice :: Maybe Integer + } + | -- | Set new price for NFT + SetPriceAct + { -- | new price for NFT (Nothing locks NFT) + act'newPrice :: Maybe Integer + } + deriving stock (Hask.Show, Generic, Hask.Eq) + deriving anyclass (FromJSON, ToJSON) + +-------------------------------------------------------------------------- +-- boiler plate instances + +PlutusTx.unstableMakeIsData ''Nft +PlutusTx.unstableMakeIsData ''UserAct +PlutusTx.unstableMakeIsData ''Act +PlutusTx.unstableMakeIsData ''NftId +PlutusTx.makeLift ''NftId diff --git a/mlabs/src/Mlabs/Plutus/Contract.hs b/mlabs/src/Mlabs/Plutus/Contract.hs new file mode 100644 index 000000000..66cc7ec39 --- /dev/null +++ b/mlabs/src/Mlabs/Plutus/Contract.hs @@ -0,0 +1,105 @@ +-- | Useful utils for contracts +module Mlabs.Plutus.Contract ( + readDatum, + readDatum', + fromDatum, + readChainIndexTxDatum, + Call, + IsEndpoint (..), + endpointName, + getEndpoint, + callSimulator, + callEndpoint', + selectForever, +) where + +import PlutusTx.Prelude +import Prelude (String) + +import Control.Lens (view, (^?)) +import Control.Monad (forever) +import Control.Monad.Freer (Eff) +import Data.Aeson (FromJSON, ToJSON) +import Data.Bifunctor (second) +import Data.Functor (void) +import Data.Map qualified as M +import Data.OpenUnion (Member) +import Data.Proxy (Proxy (..)) +import Data.Row (KnownSymbol) +import GHC.TypeLits (Symbol, symbolVal) +import Ledger (Datum (..), TxOut (txOutDatumHash), TxOutTx (txOutTxOut, txOutTxTx), lookupDatum) +import Ledger.Tx (ChainIndexTxOut, ciTxOutDatum) +import Mlabs.Data.List (maybeRight) +import Playground.Contract (Contract, ToSchema) +import Plutus.ChainIndex.Tx (ChainIndexTx, citxData) +import Plutus.Contract qualified as Contract +import Plutus.PAB.Effects.Contract.Builtin (Builtin) +import Plutus.PAB.Simulator (Simulation, callEndpointOnInstance, waitNSlots) +import Plutus.Trace.Effects.RunContract (RunContract, callEndpoint) +import Plutus.Trace.Emulator.Types (ContractConstraints, ContractHandle) +import PlutusTx (FromData, fromBuiltinData) + +-- | For off-chain code +fromDatum :: FromData a => Datum -> Maybe a +fromDatum d = do + let e = getDatum d + PlutusTx.fromBuiltinData e + +-- | For off-chain code +readDatum :: FromData a => TxOutTx -> Maybe a +readDatum txOut = do + h <- txOutDatumHash $ txOutTxOut txOut + Datum e <- lookupDatum (txOutTxTx txOut) h + PlutusTx.fromBuiltinData e + +{- | For off-chain code - from querying the chain + Using ChainIndexTxOut returned by `utxosAt` +-} +readDatum' :: FromData a => ChainIndexTxOut -> Maybe a +readDatum' txOut = do + d <- txOut ^? ciTxOutDatum + Datum e <- maybeRight d + PlutusTx.fromBuiltinData e + +{- | For off-chain code - from querying the chain + Using the ChainIndexTx returned by `utxosTxOutTxAt` +-} +readChainIndexTxDatum :: FromData a => ChainIndexTx -> [Maybe a] +readChainIndexTxDatum = fmap (snd . second (\(Datum e) -> PlutusTx.fromBuiltinData e)) . M.toList . view citxData + +type Call a = Contract.Endpoint (EndpointSymbol a) a + +class (ToSchema a, ToJSON a, FromJSON a, KnownSymbol (EndpointSymbol a)) => IsEndpoint a where + type EndpointSymbol a :: Symbol + +callEndpoint' :: + forall ep w s e effs. + (IsEndpoint ep, ContractConstraints s, Contract.HasEndpoint (EndpointSymbol ep) ep s, Member RunContract effs) => + ContractHandle w s e -> + ep -> + Eff effs () +callEndpoint' = callEndpoint @(EndpointSymbol ep) + +getEndpoint :: + forall a w s e b. + ( Contract.HasEndpoint (EndpointSymbol a) a s + , Contract.AsContractError e + , IsEndpoint a + ) => + (a -> Contract w s e b) -> + Contract.Promise w s e b +getEndpoint = Contract.endpoint @(EndpointSymbol a) + +endpointName :: forall a. IsEndpoint a => a -> String +endpointName a = symbolVal (toProxy a) + where + toProxy :: a -> Proxy (EndpointSymbol a) + toProxy _ = Proxy + +callSimulator :: IsEndpoint a => Contract.ContractInstanceId -> a -> Simulation (Builtin schema) () +callSimulator cid input = do + void $ callEndpointOnInstance cid (endpointName input) input + void $ waitNSlots 1 + +selectForever :: [Contract.Promise w s e a] -> Contract w s e b +selectForever = forever . Contract.selectList diff --git a/mlabs/src/Mlabs/Plutus/Contracts/Currency.hs b/mlabs/src/Mlabs/Plutus/Contracts/Currency.hs new file mode 100644 index 000000000..9db67fb42 --- /dev/null +++ b/mlabs/src/Mlabs/Plutus/Contracts/Currency.hs @@ -0,0 +1,184 @@ +{-# LANGUAGE NamedFieldPuns #-} + +module Mlabs.Plutus.Contracts.Currency ( + OneShotCurrency (..), + CurrencySchema, + CurrencyError (..), + AsCurrencyError (..), + curPolicy, + + -- * Actions etc + mintContract, + mintedValue, + currencySymbol, + + -- * Simple minting policy currency + SimpleMPS (..), + mintCurrency, +) where + +import Control.Lens +import PlutusTx.Prelude hiding (Monoid (..), Semigroup (..)) + +import Plutus.Contract as Contract + +import Ledger (CurrencySymbol, PaymentPubKeyHash, TxId, TxOutRef (..), getCardanoTxId, scriptCurrencySymbol) +import Ledger.Constraints qualified as Constraints +import Ledger.Contexts qualified as V +import Ledger.Scripts +import PlutusTx qualified + +import Ledger.Typed.Scripts qualified as Scripts +import Ledger.Value (TokenName, Value) +import Ledger.Value qualified as Value +import Plutus.V1.Ledger.Ada qualified as Ada + +import Data.Aeson (FromJSON, ToJSON) +import Data.Map qualified as Map +import Data.Semigroup (Last (..)) +import GHC.Generics (Generic) +import Plutus.Contracts.PubKey qualified as PubKey +import PlutusTx.AssocMap qualified as AssocMap +import Schema (ToSchema) +import Prelude (Semigroup (..)) +import Prelude qualified as Haskell + +{- HLINT ignore "Use uncurry" -} + +-- | A currency that can be created exactly once +data OneShotCurrency = OneShotCurrency + { -- | Transaction input that must be spent when + -- the currency is minted. + curRefTransactionOutput :: (TxId, Integer) + , -- | How many units of each 'TokenName' are to + -- be minted. + curAmounts :: AssocMap.Map TokenName Integer + } + deriving stock (Generic, Haskell.Show, Haskell.Eq) + deriving anyclass (ToJSON, FromJSON) + +PlutusTx.makeLift ''OneShotCurrency + +currencyValue :: CurrencySymbol -> OneShotCurrency -> Value +currencyValue s OneShotCurrency {curAmounts = amts} = + let values = map (\(tn, i) -> Value.singleton s tn i) (AssocMap.toList amts) + in fold values + +mkCurrency :: TxOutRef -> [(TokenName, Integer)] -> OneShotCurrency +mkCurrency (TxOutRef h i) amts = + OneShotCurrency + { curRefTransactionOutput = (h, i) + , curAmounts = AssocMap.fromList amts + } + +checkPolicy :: OneShotCurrency -> () -> V.ScriptContext -> Bool +checkPolicy c@(OneShotCurrency (refHash, refIdx) _) _ ctx@V.ScriptContext {V.scriptContextTxInfo = txinfo} = + let -- see note [Obtaining the currency symbol] + ownSymbol = V.ownCurrencySymbol ctx + + minted = V.txInfoMint txinfo + expected = currencyValue ownSymbol c + + -- True if the pending transaction mints the amount of + -- currency that we expect + mintOK = + let v = expected == minted + in traceIfFalse "C0" {-"Value minted different from expected"-} v + + -- True if the pending transaction spends the output + -- identified by @(refHash, refIdx)@ + txOutputSpent = + let v = V.spendsOutput txinfo refHash refIdx + in traceIfFalse "C1" {-"Pending transaction does not spend the designated transaction output"-} v + in mintOK && txOutputSpent + +curPolicy :: OneShotCurrency -> MintingPolicy +curPolicy cur = + mkMintingPolicyScript $ + $$(PlutusTx.compile [||Scripts.wrapMintingPolicy . checkPolicy||]) + `PlutusTx.applyCode` PlutusTx.liftCode cur + +{- note [Obtaining the currency symbol] +The currency symbol is the address (hash) of the validator. That is why +we can use 'Ledger.scriptAddress' here to get the symbol in off-chain code, +for example in 'mintedValue'. +Inside the validator script (on-chain) we can't use 'Ledger.scriptAddress', +because at that point we don't know the hash of the script yet. That +is why we use 'V.ownCurrencySymbol', which obtains the hash from the +'PolicyCtx' value. +-} + +-- | The 'Value' minted by the 'OneShotCurrency' contract +mintedValue :: OneShotCurrency -> Value +mintedValue cur = currencyValue (currencySymbol cur) cur + +currencySymbol :: OneShotCurrency -> CurrencySymbol +currencySymbol = scriptCurrencySymbol . curPolicy + +data CurrencyError + = PKError PubKey.PubKeyError + | CurContractError ContractError + deriving stock (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +makeClassyPrisms ''CurrencyError + +instance AsContractError CurrencyError where + _ContractError = _CurContractError + +{- | @mint [(n1, c1), ..., (n_k, c_k)]@ creates a new currency with + @k@ token names, minting @c_i@ units of each token @n_i@. + If @k == 0@ then no value is minted. A one-shot minting policy + script is used to ensure that no more units of the currency can + be minted afterwards. +-} + +{- As `Plutus.Contract.Wallet.export` doesn't support Public Key Inputs at the moment, + original plutus-use-cases contract was tweaked to make UTxO at script address first, + then use that UTxO to mint one-shot currencies. + This is the only change. +-} +mintContract :: + forall w s e. + ( AsCurrencyError e + ) => + PaymentPubKeyHash -> + [(TokenName, Integer)] -> + Contract w s e OneShotCurrency +mintContract pkh amounts = mapError (review _CurrencyError) $ do + (txOutRef, ciTxOut, pkInst) <- mapError PKError (PubKey.pubKeyContract pkh (Ada.adaValueOf 3)) + let theCurrency = mkCurrency txOutRef amounts + curVali = curPolicy theCurrency + lookups = + Constraints.mintingPolicy curVali + <> Constraints.otherData (Datum $ getRedeemer unitRedeemer) + <> Constraints.unspentOutputs (maybe Map.empty (Map.singleton txOutRef) ciTxOut) + <> Constraints.otherScript (Scripts.validatorScript pkInst) + mintTx = + Constraints.mustSpendScriptOutput txOutRef unitRedeemer + <> Constraints.mustMintValue (mintedValue theCurrency) + tx <- submitTxConstraintsWith @Scripts.Any lookups mintTx + _ <- awaitTxConfirmed (getCardanoTxId tx) + pure theCurrency + +{- | Minting policy for a currency that has a fixed amount of tokens issued + in one transaction +-} +data SimpleMPS = SimpleMPS + { tokenName :: TokenName + , amount :: Integer + } + deriving stock (Haskell.Eq, Haskell.Show, Generic) + deriving anyclass (FromJSON, ToJSON, ToSchema) + +type CurrencySchema = + Endpoint "Create native token" SimpleMPS + +-- | Use 'mintContract' to create the currency specified by a 'SimpleMPS' +mintCurrency :: + Promise (Maybe (Last OneShotCurrency)) CurrencySchema CurrencyError OneShotCurrency +mintCurrency = endpoint @"Create native token" $ \SimpleMPS {tokenName, amount} -> do + ownPK <- ownPaymentPubKeyHash + cur <- mintContract ownPK [(tokenName, amount)] + tell (Just (Last cur)) + pure cur diff --git a/mlabs/src/Mlabs/Plutus/PAB.hs b/mlabs/src/Mlabs/Plutus/PAB.hs new file mode 100644 index 000000000..8d70383b1 --- /dev/null +++ b/mlabs/src/Mlabs/Plutus/PAB.hs @@ -0,0 +1,38 @@ +module Mlabs.Plutus.PAB ( + call, + waitForLast, + printBalance, +) where + +import Prelude + +import Data.Aeson (FromJSON, Result (..), fromJSON) +import Data.Functor (void) +import Data.Monoid (Last (..)) +import Mlabs.Utils.Wallet (walletFromNumber) +import Plutus.Contract (ContractInstanceId) +import Plutus.PAB.Effects.Contract.Builtin (Builtin) +import Plutus.PAB.Simulator (Simulation, callEndpointOnInstance, valueAt, waitForState, waitNSlots) +import Wallet.Emulator.Wallet qualified as Wallet + +import Mlabs.Plutus.Contract (IsEndpoint, endpointName) +import Mlabs.System.Console.Utils (logBalance) + +call :: IsEndpoint a => ContractInstanceId -> a -> Simulation (Builtin schema) () +call cid input = do + void $ callEndpointOnInstance cid (endpointName input) input + void $ waitNSlots 2 + +{- | Waits for the given value to be written to the state of the service. + We use it to share data between endpoints. One endpoint can write parameter to state with tell + and in another endpoint we wait for the state-change. +-} +waitForLast :: FromJSON a => ContractInstanceId -> Simulation t a +waitForLast cid = + flip waitForState cid $ \json -> case fromJSON json of + Success (Last (Just x)) -> Just x + _ -> Nothing + +printBalance :: Integer -> Simulation (Builtin schema) () +printBalance n = + logBalance ("WALLET " <> show n) =<< (valueAt . Wallet.mockWalletAddress $ walletFromNumber n) diff --git a/mlabs/src/Mlabs/System/Console/PrettyLogger.hs b/mlabs/src/Mlabs/System/Console/PrettyLogger.hs new file mode 100644 index 000000000..8f644b1f1 --- /dev/null +++ b/mlabs/src/Mlabs/System/Console/PrettyLogger.hs @@ -0,0 +1,110 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE UndecidableInstances #-} + +module Mlabs.System.Console.PrettyLogger ( + LogColor (..), + LogStyle (..), + logPretty, + logPrettyColor, + logPrettyBgColor, + logPrettyColorBold, + withNewLines, + logNewLine, + logDivider, + padLeft, + padRight, +) where + +import Prelude + +import Control.Monad.IO.Class (MonadIO (..)) +import System.Console.ANSI ( + Color, + ColorIntensity (Dull, Vivid), + ConsoleIntensity (BoldIntensity), + ConsoleLayer (Background, Foreground), + SGR (Reset, SetColor, SetConsoleIntensity), + setSGR, + ) + +------------------------------------------------------------------------------- + +data LogStyle = LogStyle + { bgColor :: LogColor + , color :: LogColor + , isBold :: Bool + } + +data LogColor + = Vibrant Color + | Standard Color + | DefaultColor + +defLogStyle :: LogStyle +defLogStyle = + LogStyle {bgColor = DefaultColor, color = DefaultColor, isBold = False} + +------------------------------------------------------------------------------- + +logPretty :: MonadIO m => String -> m () +logPretty = logPrettyStyled defLogStyle + +logPrettyStyled :: MonadIO m => LogStyle -> String -> m () +logPrettyStyled style string = liftIO $ do + setSGR . foldMap ($ style) $ + [ getColorList . color + , getBgColorList . bgColor + , getConsoleIntensityList . isBold + ] + + putStr string + setSGR [Reset] + where + getColorList color = case color of + Vibrant x -> [SetColor Foreground Vivid x] + Standard x -> [SetColor Foreground Dull x] + _ -> [] + getBgColorList bgColor = case bgColor of + Vibrant x -> [SetColor Background Vivid x] + Standard x -> [SetColor Background Dull x] + _ -> [] + getConsoleIntensityList isBold = + [SetConsoleIntensity BoldIntensity | isBold] + +-- Convenience functions ------------------------------------------------------ + +logPrettyColor :: MonadIO m => LogColor -> String -> m () +logPrettyColor color = logPrettyStyled defLogStyle {color = color} + +logPrettyBgColor :: MonadIO m => Int -> LogColor -> LogColor -> String -> m () +logPrettyBgColor minWidth bgColor color str = + logPrettyStyled + defLogStyle {bgColor = bgColor, color = color} + (padRight ' ' minWidth str) + +logPrettyColorBold :: MonadIO m => LogColor -> String -> m () +logPrettyColorBold color = + logPrettyStyled defLogStyle {color = color, isBold = True} + +withNewLines :: String -> String +withNewLines string = "\n" ++ string ++ "\n" + +logNewLine :: MonadIO m => m () +logNewLine = logPretty "\n" + +logDivider :: MonadIO m => m () +logDivider = + logPretty $ + "-----------------------------------------------------------" + ++ "\n" + +padLeft :: Char -> Int -> String -> String +padLeft char len txt = replicate (len - length txt) char <> txt + +padRight :: Char -> Int -> String -> String +padRight char len txt = txt <> replicate (len - length txt) char diff --git a/mlabs/src/Mlabs/System/Console/Utils.hs b/mlabs/src/Mlabs/System/Console/Utils.hs new file mode 100644 index 000000000..a32a8365a --- /dev/null +++ b/mlabs/src/Mlabs/System/Console/Utils.hs @@ -0,0 +1,57 @@ +module Mlabs.System.Console.Utils ( + logAsciiLogo, + logAction, + logBalance, + logMlabs, +) where + +import Prelude + +import Control.Monad.IO.Class (MonadIO) +import Plutus.V1.Ledger.Value qualified as Value +import System.Console.ANSI (Color (Black, Cyan, Green, Red)) + +import Mlabs.System.Console.PrettyLogger (LogColor (Standard, Vibrant)) +import Mlabs.System.Console.PrettyLogger qualified as Pretty + +logMlabs :: MonadIO m => m () +logMlabs = logAsciiLogo (Vibrant Red) mlabs + +mlabs :: String +mlabs = + " \n\ + \ ███╗ ███╗ ██╗ █████╗ ██████╗ ███████╗ \n\ + \ ████╗ ████║ ██║ ██╔══██╗██╔══██╗██╔════╝ \n\ + \ ██╔████╔██║ ██║ ███████║██████╔╝███████╗ \n\ + \ ██║╚██╔╝██║ ██║ ██╔══██║██╔══██╗╚════██║ \n\ + \ ██║ ╚═╝ ██║ ███████╗██║ ██║██████╔╝███████║ \n\ + \ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝ " + +logAsciiLogo :: MonadIO m => LogColor -> String -> m () +logAsciiLogo color logo = do + Pretty.logNewLine + Pretty.logPrettyBgColor 40 color (Standard Black) logo + Pretty.logNewLine + +logAction :: MonadIO m => String -> m () +logAction str = Pretty.logPrettyColorBold (Vibrant Green) (Pretty.withNewLines str) + +logBalance :: MonadIO m => String -> Value.Value -> m () +logBalance wallet val = do + Pretty.logNewLine + Pretty.logPrettyBgColor 40 (Vibrant Cyan) (Standard Black) (wallet ++ " BALANCE") + Pretty.logNewLine + Pretty.logPrettyColor (Vibrant Cyan) (formatValue val) + Pretty.logNewLine + +formatValue :: Value.Value -> String +formatValue v = + unlines $ + fmap formatTokenValue $ + filter ((/= 0) . (\(_, _, n) -> n)) $ Value.flattenValue v + where + formatTokenValue (_, name, value) = + case name of + "" -> Pretty.padRight ' ' 7 "Ada" ++ " " ++ show value + -- (Value.TokenName n) -> Pretty.padRight ' ' 7 $ Char8.unpack n ++ " " ++ show value + (Value.TokenName n) -> Pretty.padRight ' ' 7 $ show n ++ " " ++ show value diff --git a/mlabs/src/Mlabs/Utils/Wallet.hs b/mlabs/src/Mlabs/Utils/Wallet.hs new file mode 100644 index 000000000..7399e65eb --- /dev/null +++ b/mlabs/src/Mlabs/Utils/Wallet.hs @@ -0,0 +1,10 @@ +module Mlabs.Utils.Wallet ( + walletFromNumber, +) where + +import Ledger.CardanoWallet (WalletNumber (..)) +import PlutusTx.Prelude +import Wallet.Emulator.Wallet (Wallet, fromWalletNumber) + +walletFromNumber :: Integer -> Wallet +walletFromNumber = fromWalletNumber . WalletNumber diff --git a/mlabs/stack.yaml b/mlabs/stack.yaml new file mode 100644 index 000000000..46ef97b6f --- /dev/null +++ b/mlabs/stack.yaml @@ -0,0 +1,172 @@ +resolver: lts-17.14 + +nix: + packages: + - cacert # Fixes "SSL certificate problem: unable to get local issuer certificate" + - zlib + +packages: +- . + +extra-deps: +- git: https://github.com/input-output-hk/plutus.git + commit: 926a49d16439b693648b68b7e6eb7877a5e622e4 + subdirs: + - playground-common + - plutus-chain-index + - plutus-core + - plutus-contract + - plutus-ledger + - plutus-tx + - plutus-tx-plugin + - prettyprinter-configurable + - plutus-ledger-api + - plutus-ledger-constraints + - plutus-pab + - plutus-use-cases + - freer-extras + - quickcheck-dynamic + - word-array +# Flat compression +- pure-zlib-0.6.7@sha256:5a1cdf87bf3079b7d3abace1f94eeb3c597c687a38a08ee2908783e609271467,3487 +# FEAT/NEAT and deps +- lazy-search-0.1.2.0 +- size-based-0.1.2.0 +- testing-feat-1.1.0.0 +- Stream-0.4.7.2@sha256:ed78165aa34c4e23dc53c9072f8715d414a585037f2145ea0eb2b38300354c53,1009 +- lazysmallcheck-0.6@sha256:dac7a1e4877681f1260309e863e896674dd6efc1159897b7945893e693f2a6bc,1696 +# Other missing packages +- composition-prelude-3.0.0.2 +- constraints-extras-0.3.0.2 +- dependent-map-0.4.0.0 +- dependent-sum-0.7.1.0 +- dependent-sum-template-0.1.0.3 +- eventful-memory-0.2.0 +- barbies-2.0.2.0 +- nothunks-0.1.2 +- indexed-traversable-instances-0.1 +- base16-bytestring-1.0.1.0 +# A revision was added to keep the bounds down, we don't actually want this! +# we work around the newer persistent-template by adding flags below +- eventful-sql-common-0.2.0@rev:0 +- eventful-sqlite-0.2.0 +- monoidal-containers-0.6.0.1 +- recursion-schemes-5.1.3 +- row-types-0.4.0.0 +- time-out-0.2@sha256:b9a6b4dee64f030ecb2a25dca0faff39b3cb3b5fefbb8af3cdec4142bfd291f2 +- time-interval-0.1.1@sha256:7bfd3601853d1af7caa18248ec10b01701d035ac274a93bb4670fea52a14d4e8 +- time-units-1.0.0@sha256:27cf54091c4a0ca73d504fc11d5c31ab4041d17404fe3499945e2055697746c1 +- servant-websockets-2.0.0 +- servant-subscriber-0.7.0.0 +- safe-exceptions-checked-0.1.0 +- async-timer-0.1.4.1 +- sbv-8.9 +- wl-pprint-1.2.1@sha256:aea676cff4a062d7d912149d270e33f5bb0c01b68a9db46ff13b438141ff4b7c +- witherable-0.4.1 +- canonical-json-0.6.0.0@sha256:9021f435ccb884a3b4c55bcc6b50eb19d5fc3cc3f29d5fcbdef016f5bbae23a2,3488 +- statistics-linreg-0.3@sha256:95c6efe6c7f6b26bc6e9ada90ab2d18216371cf59a6ef2b517b4a6fd35d9a76f,2544 +- partial-order-0.2.0.0@sha256:a0d6ddc9ebcfa965a5cbcff1d06d46a79d44ea5a0335c583c2a51bcb41334487,2275 +- streaming-binary-0.2.2.0@sha256:09b9a9b0291199c5808e88dcf9c93e7b336e740c71efeafd7c835b59794a8c90,1034 +- transformers-except-0.1.1@sha256:6c12ef8e632a10440968cd541e75074bd6ef4b5ff4012677f8f8189d7b2d0df6,1387 +- beam-core-0.9.0.0@sha256:e5b1cb4d5b8a8a166f3373e8718672a3884feb9a5a133404b047b0af76538023,5282 +- beam-migrate-0.5.0.0@sha256:d3f7e333ec9e96122ccec6be0d38a88f766dfc248323be73fd0b3cee245ea421,4923 +- beam-sqlite-0.5.0.0@sha256:d785bf40101235a72b80652ce27be9c8048de5f7c171ccb23e1e62b8f1ce6e7c,3496 + +# cabal.project is the source of truth for these pins, they are explained there +# and need to be kept in sync. +- git: https://github.com/Quid2/flat.git + commit: 95e5d7488451e43062ca84d5376b3adcc465f1cd +- git: https://github.com/shmish111/purescript-bridge.git + commit: 6a92d7853ea514be8b70bab5e72077bf5a510596 +- git: https://github.com/shmish111/servant-purescript.git + commit: a76104490499aa72d40c2790d10e9383e0dbde63 +- git: https://github.com/input-output-hk/cardano-crypto.git + commit: ce8f1934e4b6252084710975bd9bbc0a4648ece4 +- git: https://github.com/input-output-hk/ouroboros-network + commit: e338f2cf8e1078fbda9555dd2b169c6737ef6774 + subdirs: + - monoidal-synchronisation + - typed-protocols + - typed-protocols-examples + - ouroboros-network + - ouroboros-network-testing + - ouroboros-network-framework + - ouroboros-consensus + - ouroboros-consensus-byron + - ouroboros-consensus-cardano + - ouroboros-consensus-shelley + - io-sim + - io-classes + - network-mux +- git: https://github.com/input-output-hk/cardano-prelude + commit: fd773f7a58412131512b9f694ab95653ac430852 + subdirs: + - cardano-prelude + - cardano-prelude-test +- git: https://github.com/input-output-hk/cardano-base + commit: a715c7f420770b70bbe95ca51d3dec83866cb1bd + subdirs: + - binary + - binary/test + - slotting + - cardano-crypto-class + - cardano-crypto-tests + - cardano-crypto-praos + - strict-containers +- git: https://github.com/input-output-hk/cardano-ledger-specs + commit: b8f1ebb46a91f1c634e616feb89ae34de5937e17 + subdirs: + - byron/chain/executable-spec + - byron/crypto + - byron/crypto/test + - byron/ledger/executable-spec + - byron/ledger/impl + - byron/ledger/impl/test + - semantics/executable-spec + - semantics/small-steps-test + - shelley/chain-and-ledger/dependencies/non-integer + - shelley/chain-and-ledger/executable-spec + - shelley/chain-and-ledger/shelley-spec-ledger-test + - shelley-ma/impl + - cardano-ledger-core + - alonzo/impl +- git: https://github.com/input-output-hk/iohk-monitoring-framework + commit: 34abfb7f4f5610cabb45396e0496472446a0b2ca + subdirs: + - contra-tracer + - iohk-monitoring + - tracer-transformers + - plugins/backend-ekg + - plugins/backend-aggregation + - plugins/backend-monitoring + - plugins/backend-trace-forwarder + - plugins/scribe-systemd +- git: https://github.com/input-output-hk/cardano-node.git + commit: f3ef4ed72894499160f2330b91572a159005c148 + subdirs: + - cardano-api + - cardano-cli + - cardano-node + - cardano-config +- git: https://github.com/input-output-hk/Win32-network + commit: 94153b676617f8f33abe8d8182c37377d2784bd1 +- git: https://github.com/input-output-hk/hedgehog-extras + commit: 8bcd3c9dc22cc44f9fcfe161f4638a384fc7a187 +- git: https://github.com/input-output-hk/goblins + commit: cde90a2b27f79187ca8310b6549331e59595e7ba + +# More missing packages, that were not present in `stack.yaml` in plutus repository +- Unique-0.4.7.8 +- moo-1.2 +- gray-code-0.3.1 +- libsystemd-journal-1.4.5 + + +allow-newer: true + +extra-package-dbs: [] + +ghc-options: + # Newer versions of persistent-template require some extra language extensions. Fortunately + # we can hack around this here rather than having to fork eventful & co (for now) + eventful-sql-common: "-XDerivingStrategies -XStandaloneDeriving -XUndecidableInstances -XDataKinds -XFlexibleInstances -XMultiParamTypeClasses" diff --git a/mlabs/test/Main.hs b/mlabs/test/Main.hs new file mode 100644 index 000000000..3c57ab62d --- /dev/null +++ b/mlabs/test/Main.hs @@ -0,0 +1,62 @@ +module Main (main) where + +import PlutusTx.Prelude +import Prelude (IO) + +import Plutus.Test.Model (readDefaultBchConfig) +import Test.Tasty (defaultMain, testGroup) + +import Test.EfficientNFT.Plutip qualified as ENFT.Plutip +import Test.EfficientNFT.Quickcheck qualified as ENFT.Quickcheck +import Test.EfficientNFT.Resources qualified as ENFT.Resources +import Test.EfficientNFT.Script.FeeWithdraw qualified as ENFT.FeeWithdraw +import Test.EfficientNFT.Script.TokenBurn qualified as ENFT.TokenBurn +import Test.EfficientNFT.Script.TokenChangeOwner qualified as ENFT.TokenChangeOwner +import Test.EfficientNFT.Script.TokenChangePrice qualified as ENFT.TokenChangePrice +import Test.EfficientNFT.Script.TokenMarketplaceBuy qualified as ENFT.TokenMarketplaceBuy +import Test.EfficientNFT.Script.TokenMarketplaceRedeem qualified as ENFT.TokenMarketplaceRedeem +import Test.EfficientNFT.Script.TokenMarketplaceSetPrice qualified as ENFT.TokenMarketplaceSetPrice +import Test.EfficientNFT.Script.TokenMint qualified as ENFT.TokenMint +import Test.EfficientNFT.Script.TokenRestake qualified as ENFT.TokenRestake +import Test.EfficientNFT.Script.TokenUnstake qualified as ENFT.TokenUnstake +import Test.EfficientNFT.Size qualified as ENFT.Size +import Test.EfficientNFT.Trace qualified as ENFT.Trace +import Test.NFT.Size qualified as NFT.Size + +main :: IO () +main = do + cfg <- readDefaultBchConfig + defaultMain $ + testGroup + "tests" + [ testGroup + "NFT" + [ NFT.Size.test + ] + , testGroup + "Efficient NFT" + [ ENFT.Size.test + , ENFT.Resources.test cfg + , testGroup + "Token" + [ ENFT.TokenMint.test + , ENFT.TokenChangeOwner.test + , ENFT.TokenChangePrice.test + , ENFT.TokenBurn.test + ] + , testGroup + "Staking" + [ ENFT.TokenUnstake.test + , ENFT.TokenRestake.test + ] + , testGroup + "Marketplace" + [ ENFT.TokenMarketplaceSetPrice.test + , ENFT.TokenMarketplaceBuy.test + , ENFT.TokenMarketplaceRedeem.test + ] + , ENFT.FeeWithdraw.test + , ENFT.Quickcheck.test + , ENFT.Plutip.test + ] + ] diff --git a/mlabs/test/Test/Demo/Contract/Mint.hs b/mlabs/test/Test/Demo/Contract/Mint.hs new file mode 100644 index 000000000..881d4587f --- /dev/null +++ b/mlabs/test/Test/Demo/Contract/Mint.hs @@ -0,0 +1,86 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE NoImplicitPrelude #-} + +module Test.Demo.Contract.Mint ( + test, +) where + +import PlutusTx.Prelude + +import Control.Lens ((&), (.~)) +import Control.Monad (void) +import Data.Default (def) +import Data.Map qualified as Map +import Ledger.Ada (lovelaceValueOf) +import Ledger.Value (AssetClass (..), TokenName, Value, assetClassValue) +import Plutus.Contract.Test qualified as Test +import Plutus.Trace.Emulator as Emulator +import Test.Tasty (TestTree) + +import Mlabs.Demo.Contract.Mint (MintParams (..), curSymbol, mintEndpoints) +import Mlabs.Utils.Wallet (walletFromNumber) + +wallet1 = walletFromNumber 1 +wallet2 = walletFromNumber 2 + +test :: TestTree +test = + Test.checkPredicateOptions + (Test.defaultCheckOptions & Test.emulatorConfig .~ emCfg) + "mint trace" + ( Test.walletFundsChange + wallet1 + (lovelaceValueOf (-15_000_000) <> assetClassValue usdToken 15) + Test..&&. Test.walletFundsChange + wallet2 + ( lovelaceValueOf (-50_000_000) + <> assetClassValue usdToken 20 + <> assetClassValue cadToken 30 + ) + ) + mintTrace + +emCfg :: EmulatorConfig +emCfg = + EmulatorConfig + (Left $ Map.fromList [(wallet1, v), (wallet2, v)]) + def + def + where + v :: Value + v = lovelaceValueOf 100_000_000 + +usd :: TokenName +usd = "USD" + +cad :: TokenName +cad = "CAD" + +usdToken :: AssetClass +usdToken = AssetClass (curSymbol, usd) + +cadToken :: AssetClass +cadToken = AssetClass (curSymbol, cad) + +mintTrace :: EmulatorTrace () +mintTrace = do + h1 <- activateContractWallet wallet1 mintEndpoints + h2 <- activateContractWallet wallet2 mintEndpoints + + -- Scenario 1: Buy single currency. + callEndpoint @"mint" h1 MintParams {mpTokenName = usd, mpAmount = 5} + void $ Emulator.waitNSlots 2 + callEndpoint @"mint" h1 MintParams {mpTokenName = usd, mpAmount = 10} + void $ Emulator.waitNSlots 2 + + -- Scenario 2: Buy multiple currencies. + callEndpoint @"mint" h2 MintParams {mpTokenName = usd, mpAmount = 20} + void $ Emulator.waitNSlots 2 + callEndpoint @"mint" h2 MintParams {mpTokenName = cad, mpAmount = 30} + void $ Emulator.waitNSlots 2 diff --git a/mlabs/test/Test/EfficientNFT/Plutip.hs b/mlabs/test/Test/EfficientNFT/Plutip.hs new file mode 100644 index 000000000..a03244d13 --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Plutip.hs @@ -0,0 +1,137 @@ +{-# OPTIONS_GHC -Wno-unused-do-bind #-} + +module Test.EfficientNFT.Plutip (test) where + +import Prelude hiding (toEnum) + +import Control.Monad.Reader (ReaderT) +import Data.List.NonEmpty (NonEmpty) +import Data.Monoid (Last) +import Data.Text (Text) +import Ledger (PaymentPubKeyHash (unPaymentPubKeyHash), Value) +import Plutus.Contract (waitNSlots) +import PlutusTx.Enum (toEnum) +import Test.Plutip.Contract (initAda, shouldFail, shouldSucceed, withContractAs) +import Test.Plutip.Internal.Types (ClusterEnv, ExecutionResult (ExecutionResult)) +import Test.Plutip.LocalCluster (BpiWallet, withCluster) +import Test.Tasty (TestTree) + +import Control.Monad (void) +import Mlabs.EfficientNFT.Contract.Burn (burn) +import Mlabs.EfficientNFT.Contract.FeeWithdraw (feeWithdraw) +import Mlabs.EfficientNFT.Contract.MarketplaceBuy (marketplaceBuy) +import Mlabs.EfficientNFT.Contract.MarketplaceDeposit (marketplaceDeposit) +import Mlabs.EfficientNFT.Contract.MarketplaceRedeem (marketplaceRedeem) +import Mlabs.EfficientNFT.Contract.MarketplaceSetPrice (marketplaceSetPrice) +import Mlabs.EfficientNFT.Contract.Mint (generateNft, mintWithCollection) +import Mlabs.EfficientNFT.Contract.SetPrice (setPrice) +import Mlabs.EfficientNFT.Types (MintParams (MintParams), NftData, SetPriceParams (SetPriceParams)) + +-- TODO: Partial value asserts here when added (https://github.com/mlabs-haskell/plutip/issues/42) +test :: TestTree +test = + withCluster + "Integration tests" + [ shouldSucceed "Happy path" (initAda 100 <> initAda 100 <> initAda 100) testValid + , shouldFail "Fail to change price when not owner" (initAda 100 <> initAda 100) testChangePriceNotOwner + , shouldFail "Fail to redeem when not owner" (initAda 100 <> initAda 100) testRedeemNotOwner + , shouldFail "Fail unlocking too early" (initAda 100) testBurnTooEarly + ] + +type TestCase = ReaderT (ClusterEnv, NonEmpty BpiWallet) IO (ExecutionResult (Last NftData) Text ((), NonEmpty Value)) + +testValid :: TestCase +testValid = do + (ExecutionResult (Right ((nft3, pkhs), _)) _) <- withContractAs 0 $ \[_, pkh] -> do + let pkhs = pure $ unPaymentPubKeyHash pkh + cnft <- generateNft + waitNSlots 1 + + nft1 <- mintWithCollection (cnft, MintParams (toEnum 0) (toEnum 50_00) (toEnum 10_000_000) 5 5 Nothing pkhs) + waitNSlots 1 + + nft2 <- setPrice (SetPriceParams nft1 (toEnum 50_000_000)) + waitNSlots 1 + + nft3 <- marketplaceDeposit nft2 + waitNSlots 1 + + pure (nft3, pkhs) + + withContractAs 1 $ + const $ do + nft4 <- marketplaceBuy nft3 + waitNSlots 1 + + nft5 <- marketplaceSetPrice (SetPriceParams nft4 (toEnum 25_000_000)) + waitNSlots 1 + + nft6 <- marketplaceRedeem nft5 + waitNSlots 1 + + nft7 <- setPrice (SetPriceParams nft6 (toEnum 20_000_000)) + waitNSlots 1 + + burn nft7 + waitNSlots 1 + + withContractAs 2 $ + const $ do + feeWithdraw pkhs + void $ waitNSlots 1 + +testChangePriceNotOwner :: TestCase +testChangePriceNotOwner = do + (ExecutionResult (Right (nft2, _)) _) <- withContractAs 0 $ + const $ do + cnft <- generateNft + waitNSlots 1 + + nft1 <- mintWithCollection (cnft, MintParams (toEnum 0) (toEnum 0) (toEnum 10_000_000) 5 5 Nothing []) + waitNSlots 1 + + nft2 <- marketplaceDeposit nft1 + waitNSlots 1 + + pure nft2 + + withContractAs 1 $ + const $ do + marketplaceSetPrice (SetPriceParams nft2 (toEnum 20_000_000)) + waitNSlots 1 + + pure () + +testRedeemNotOwner :: TestCase +testRedeemNotOwner = do + (ExecutionResult (Right (nft2, _)) _) <- withContractAs 0 $ + const $ do + cnft <- generateNft + waitNSlots 1 + + nft1 <- mintWithCollection (cnft, MintParams (toEnum 0) (toEnum 0) (toEnum 10_000_000) 5 5 Nothing []) + waitNSlots 1 + + nft2 <- marketplaceDeposit nft1 + waitNSlots 1 + + pure nft2 + + withContractAs 1 $ + const $ do + marketplaceRedeem nft2 + waitNSlots 1 + + pure () + +testBurnTooEarly :: TestCase +testBurnTooEarly = do + withContractAs 0 $ + const $ do + cnft <- generateNft + waitNSlots 1 + + nft1 <- mintWithCollection (cnft, MintParams (toEnum 0) (toEnum 0) (toEnum 10_000_000) 5_000_000_000 5_000_000_000 Nothing []) + waitNSlots 1 + + burn nft1 diff --git a/mlabs/test/Test/EfficientNFT/Quickcheck.hs b/mlabs/test/Test/EfficientNFT/Quickcheck.hs new file mode 100644 index 000000000..9bc884a98 --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Quickcheck.hs @@ -0,0 +1,360 @@ +{-# LANGUAGE GADTs #-} + +module Test.EfficientNFT.Quickcheck (test) where + +import Control.Lens (makeLenses, view, (&), (.~), (^.)) +import Control.Monad (void, when) +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.Monoid (Last (..)) +import Data.Set (Set) +import Data.Set qualified as Set +import Data.Text (Text) +import Ledger (AssetClass, PaymentPubKeyHash (PaymentPubKeyHash), PubKeyHash, ValidatorHash (ValidatorHash), minAdaTxOut, scriptCurrencySymbol, unPaymentPubKeyHash) +import Ledger.Typed.Scripts (validatorHash) +import Mlabs.Utils.Wallet (walletFromNumber) +import Plutus.Contract.Test (CheckOptions, Wallet (..), defaultCheckOptions, emulatorConfig, mockWalletPaymentPubKeyHash) +import Plutus.Contract.Test.ContractModel ( + Action, + Actions, + ContractInstanceSpec (..), + ContractModel (..), + contractState, + defaultCoverageOptions, + deposit, + getModelState, + propRunActionsWithOptions, + transfer, + wait, + withdraw, + ($=), + ($~), + ) +import Plutus.Trace.Emulator (callEndpoint, initialChainState) +import Plutus.Trace.Emulator qualified as Trace +import Plutus.V1.Ledger.Ada (adaSymbol, adaToken, getLovelace, lovelaceValueOf, toValue) +import Plutus.V1.Ledger.Value (CurrencySymbol (CurrencySymbol), Value, assetClass, assetClassValue, singleton, unAssetClass) +import PlutusTx.Natural (Natural) +import PlutusTx.Prelude hiding ((<$>), (<*>), (==)) +import Test.QuickCheck qualified as QC +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.QuickCheck (testProperty) +import Prelude ((<$>), (<*>)) +import Prelude qualified as Hask + +import Mlabs.EfficientNFT.Api (NFTAppSchema, endpoints) +import Mlabs.EfficientNFT.Dao (daoValidator) +import Mlabs.EfficientNFT.Lock (lockValidator) +import Mlabs.EfficientNFT.Token (mkTokenName, policy) +import Mlabs.EfficientNFT.Types + +data MockInfo = MockInfo + { _mock'owner :: Wallet + , _mock'author :: Wallet + } + deriving (Hask.Show, Hask.Eq) +makeLenses ''MockInfo + +data NftModel = NftModel + { -- | Map of NFTs and owners + _mNfts :: Map NftData MockInfo + , -- | + _mMarketplace :: Map NftData MockInfo + , -- | Preminted not used collection NFTs + _mUnusedCollections :: Set AssetClass + , _mLockedFees :: Integer + } + deriving (Hask.Show, Hask.Eq) +makeLenses ''NftModel + +instance ContractModel NftModel where + data Action NftModel + = ActionMint + { aAuthor :: Wallet + , aPrice :: Natural + , aAuthorShare :: Natural + , aDaoShare :: Natural + , aCollection :: AssetClass + } + | ActionSetPrice + { aNftData :: NftData + , aMockInfo :: MockInfo + , aPrice :: Natural + } + | ActionMarketplaceDeposit + { aNftData :: NftData + , aMockInfo :: MockInfo + } + | ActionMarketplaceRedeem + { aNftData :: NftData + , aMockInfo :: MockInfo + } + | ActionMarketplaceSetPrice + { aNftData :: NftData + , aMockInfo :: MockInfo + , aPrice :: Natural + } + | ActionMarketplaceBuy + { aNftData :: NftData + , aMockInfo :: MockInfo + , aNewOwner :: Wallet + } + | ActionFeeWithdraw + { aPerformer :: Wallet -- TODO: better name + } + deriving (Hask.Show, Hask.Eq) + + data ContractInstanceKey NftModel w s e where + UserKey :: Wallet -> ContractInstanceKey NftModel (Last NftData) NFTAppSchema Text + + initialHandleSpecs = Hask.fmap (\w -> ContractInstanceSpec (UserKey w) w endpoints) (wallets <> feeValultKeys) + + initialState = NftModel Hask.mempty Hask.mempty (Set.fromList hardcodedCollections) 0 + + arbitraryAction model = + let genWallet = QC.elements wallets + genNonNeg = toEnum . (* 1_000_000) . (+ 25) . QC.getNonNegative <$> QC.arbitrary + genShare = toEnum <$> QC.elements [0 .. 4500] + genNftId = QC.elements $ addNonExistingNFT $ Map.toList (model ^. contractState . mNfts) + genMarketplaceNftId = QC.elements $ addNonExistingNFT $ Map.toList (model ^. contractState . mMarketplace) + genCollection = QC.elements hardcodedCollections + -- We need this hack cause `QC.elements` cannot take an empty list. + -- It will be filtered out in `precondition` check + addNonExistingNFT = ((NftData nonExistingCollection nonExsistingNFT, MockInfo w1 w1) :) + in QC.oneof + [ ActionMint + <$> genWallet + <*> genNonNeg + <*> genShare + <*> genShare + <*> genCollection + , uncurry ActionSetPrice + <$> genNftId + <*> genNonNeg + , uncurry ActionMarketplaceDeposit + <$> genNftId + , uncurry ActionMarketplaceRedeem + <$> genMarketplaceNftId + , uncurry ActionMarketplaceSetPrice + <$> genMarketplaceNftId + <*> genNonNeg + , uncurry ActionMarketplaceBuy + <$> genMarketplaceNftId + <*> genWallet + , ActionFeeWithdraw + <$> QC.elements feeValultKeys + ] + + precondition s ActionMint {..} = + Set.member aCollection (s ^. contractState . mUnusedCollections) + precondition s ActionSetPrice {..} = + not (Map.null $ s ^. contractState . mNfts) + && Map.member aNftData (s ^. contractState . mNfts) + && aPrice /= nftId'price (nftData'nftId aNftData) + precondition s ActionMarketplaceDeposit {..} = + not (Map.null $ s ^. contractState . mNfts) + && Map.member aNftData (s ^. contractState . mNfts) + precondition s ActionMarketplaceRedeem {..} = + not (Map.null $ s ^. contractState . mMarketplace) + && Map.member aNftData (s ^. contractState . mMarketplace) + precondition s ActionMarketplaceSetPrice {..} = + not (Map.null $ s ^. contractState . mMarketplace) + && Map.member aNftData (s ^. contractState . mMarketplace) + && aPrice /= nftId'price (nftData'nftId aNftData) + precondition s ActionMarketplaceBuy {..} = + not (Map.null $ s ^. contractState . mMarketplace) + && Map.member aNftData (s ^. contractState . mMarketplace) + && mockWalletPaymentPubKeyHash aNewOwner /= nftId'owner (nftData'nftId aNftData) + precondition s ActionFeeWithdraw {} = + (s ^. contractState . mLockedFees) > 0 + + perform h _ ActionMint {..} = do + let params = MintParams aAuthorShare aDaoShare aPrice 5 5 Nothing feeValultKeys' + callEndpoint @"mint-with-collection" (h $ UserKey aAuthor) (aCollection, params) + void $ Trace.waitNSlots 5 + perform h _ ActionSetPrice {..} = do + let params = SetPriceParams aNftData aPrice + callEndpoint @"set-price" (h $ UserKey (aMockInfo ^. mock'owner)) params + void $ Trace.waitNSlots 5 + perform h _ ActionMarketplaceDeposit {..} = do + callEndpoint @"marketplace-deposit" (h $ UserKey (aMockInfo ^. mock'owner)) aNftData + void $ Trace.waitNSlots 5 + perform h _ ActionMarketplaceRedeem {..} = do + callEndpoint @"marketplace-redeem" (h $ UserKey (aMockInfo ^. mock'owner)) aNftData + void $ Trace.waitNSlots 5 + perform h _ ActionMarketplaceSetPrice {..} = do + let params = SetPriceParams aNftData aPrice + callEndpoint @"marketplace-set-price" (h $ UserKey (aMockInfo ^. mock'owner)) params + void $ Trace.waitNSlots 5 + perform h _ ActionMarketplaceBuy {..} = do + callEndpoint @"marketplace-buy" (h $ UserKey aNewOwner) aNftData + void $ Trace.waitNSlots 5 + perform h _ ActionFeeWithdraw {..} = do + callEndpoint @"fee-withdraw" (h $ UserKey aPerformer) feeValultKeys' + void $ Trace.waitNSlots 5 + + nextState ActionMint {..} = do + wait 1 + let nft = + NftId + { nftId'price = aPrice + , nftId'owner = mockWalletPaymentPubKeyHash aAuthor + , nftId'collectionNftTn = snd . unAssetClass $ aCollection + } + collection = + NftCollection + { nftCollection'collectionNftCs = fst . unAssetClass $ aCollection + , nftCollection'lockLockup = 5 -- 7776000 + , nftCollection'lockLockupEnd = 5 -- 7776000 + , nftCollection'lockingScript = + validatorHash $ lockValidator (fst $ unAssetClass aCollection) 5 5 -- 7776000 7776000 + , nftCollection'author = mockWalletPaymentPubKeyHash aAuthor + , nftCollection'authorShare = aAuthorShare + , nftCollection'daoScript = + validatorHash $ daoValidator feeValultKeys' + , nftCollection'daoShare = aDaoShare + } + nftData = NftData collection nft + curr = getCurr nftData + mNfts $~ Map.insert nftData (MockInfo aAuthor aAuthor) + mUnusedCollections $~ Set.delete aCollection + deposit aAuthor $ singleton curr (mkTokenName nft) 1 + withdraw aAuthor (toValue minAdaTxOut <> assetClassValue aCollection 1) + wait 4 + nextState ActionSetPrice {..} = do + let oldNft = nftData'nftId aNftData + newNft = oldNft {nftId'price = aPrice} + collection = nftData'nftCollection aNftData + wal = aMockInfo ^. mock'owner + curr = getCurr aNftData + mNfts $~ (Map.insert (NftData collection newNft) aMockInfo . Map.delete aNftData) + deposit wal $ singleton curr (mkTokenName newNft) 1 + withdraw wal $ singleton curr (mkTokenName oldNft) 1 + wait 5 + nextState ActionMarketplaceDeposit {..} = do + let wal = aMockInfo ^. mock'owner + curr = getCurr aNftData + nft = nftData'nftId aNftData + mNfts $~ Map.delete aNftData + mMarketplace $~ Map.insert aNftData aMockInfo + withdraw wal (singleton curr (mkTokenName nft) 1 <> toValue minAdaTxOut) + wait 5 + nextState ActionMarketplaceRedeem {..} = do + let wal = aMockInfo ^. mock'owner + curr = getCurr aNftData + newPrice = toEnum (fromEnum (nftId'price oldNft) + 1) + oldNft = nftData'nftId aNftData + newNft = oldNft {nftId'price = newPrice} + collection = nftData'nftCollection aNftData + mNfts $~ Map.insert (NftData collection newNft) aMockInfo + mMarketplace $~ Map.delete aNftData + deposit wal (singleton curr (mkTokenName newNft) 1 <> toValue minAdaTxOut) + wait 5 + nextState ActionMarketplaceSetPrice {..} = do + let oldNft = nftData'nftId aNftData + newNft = oldNft {nftId'price = aPrice} + collection = nftData'nftCollection aNftData + mMarketplace $~ (Map.insert (NftData collection newNft) aMockInfo . Map.delete aNftData) + wait 5 + nextState ActionMarketplaceBuy {..} = do + let oldNft = nftData'nftId aNftData + newNft = oldNft {nftId'owner = mockWalletPaymentPubKeyHash aNewOwner} + collection = nftData'nftCollection aNftData + newInfo = mock'owner .~ aNewOwner $ aMockInfo + nftPrice = nftId'price oldNft + getShare share = (fromEnum nftPrice * share) `divide` 100_00 + authorShare = getShare (fromEnum . nftCollection'authorShare $ collection) + daoShare = getShare (fromEnum . nftCollection'daoShare $ collection) + ownerShare = lovelaceValueOf (fromEnum nftPrice - filterLow authorShare - filterLow daoShare) + filterLow v + | fromEnum v < getLovelace minAdaTxOut = 0 + | otherwise = fromEnum v + moreThanMinAda v = + v > getLovelace minAdaTxOut + mMarketplace $~ (Map.insert (NftData collection newNft) newInfo . Map.delete aNftData) + when (moreThanMinAda authorShare) $ transfer aNewOwner (aMockInfo ^. mock'author) (lovelaceValueOf authorShare) + when (moreThanMinAda daoShare) $ do + withdraw aNewOwner (lovelaceValueOf daoShare) + mLockedFees $~ (+ daoShare) + transfer aNewOwner (aMockInfo ^. mock'owner) ownerShare + wait 5 + nextState ActionFeeWithdraw {..} = do + s <- view contractState <$> getModelState + deposit aPerformer $ lovelaceValueOf (s ^. mLockedFees) + mLockedFees $= 0 + +deriving instance Hask.Eq (ContractInstanceKey NftModel w s e) +deriving instance Hask.Show (ContractInstanceKey NftModel w s e) + +getCurr :: NftData -> CurrencySymbol +getCurr nft = + let policy' = policy . nftData'nftCollection $ nft + in scriptCurrencySymbol policy' + +hardcodedCollections :: [AssetClass] +hardcodedCollections = [assetClass cs tn | cs <- ["aa", "bb"], tn <- ["NFT1", "NFT2"]] + +w1, w2, w3, w4, w5 :: Wallet +w1 = walletFromNumber 1 +w2 = walletFromNumber 2 +w3 = walletFromNumber 3 +w4 = walletFromNumber 4 +w5 = walletFromNumber 5 + +wallets :: [Wallet] +wallets = [w1, w2, w3] + +feeValultKeys :: [Wallet] +feeValultKeys = [w4, w5] + +feeValultKeys' :: [PubKeyHash] +feeValultKeys' = fmap (unPaymentPubKeyHash . mockWalletPaymentPubKeyHash) feeValultKeys + +propContract :: Actions NftModel -> QC.Property +propContract = + QC.withMaxSuccess 100 + . propRunActionsWithOptions + checkOptions + defaultCoverageOptions + (const $ Hask.pure True) + +checkOptions :: CheckOptions +checkOptions = defaultCheckOptions & emulatorConfig . initialChainState .~ Left initialDistribution + +initialDistribution :: Map Wallet Value +initialDistribution = + Map.fromList $ + fmap (,vals) (wallets <> feeValultKeys) + where + vals = + singleton adaSymbol adaToken 100_000_000_000 + <> mconcat (fmap (`assetClassValue` 1) hardcodedCollections) + +nonExsistingNFT :: NftId +nonExsistingNFT = + NftId + { nftId'price = toEnum 0 + , nftId'owner = PaymentPubKeyHash "" + , nftId'collectionNftTn = "" + } + +nonExistingCollection :: NftCollection +nonExistingCollection = + NftCollection + { nftCollection'collectionNftCs = CurrencySymbol "ff" + , nftCollection'lockLockup = 0 + , nftCollection'lockLockupEnd = 0 + , nftCollection'lockingScript = ValidatorHash "" + , nftCollection'author = PaymentPubKeyHash "" + , nftCollection'authorShare = toEnum 0 + , nftCollection'daoScript = ValidatorHash "" + , nftCollection'daoShare = toEnum 0 + } + +test :: TestTree +test = + testGroup + "QuickCheck" + [ -- testProperty "Can get funds out" propNoLockedFunds + testProperty "Contract" propContract + ] diff --git a/mlabs/test/Test/EfficientNFT/Resources.hs b/mlabs/test/Test/EfficientNFT/Resources.hs new file mode 100644 index 000000000..b9f6e5d55 --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Resources.hs @@ -0,0 +1,352 @@ +module Test.EfficientNFT.Resources (test) where + +import Prelude hiding (fromEnum, toEnum) + +import Control.Monad (void, (<=<)) +import Control.Monad.State.Strict (modify) +import Data.Default (def) +import Data.List (find) +import Data.Maybe (fromJust) +import Ledger ( + Extended (Finite, PosInf), + Interval (Interval), + LowerBound (LowerBound), + PaymentPubKeyHash (PaymentPubKeyHash), + PubKeyHash, + UpperBound (UpperBound), + minAdaTxOut, + scriptCurrencySymbol, + txOutValue, + unPaymentPubKeyHash, + ) +import Ledger.TimeSlot (slotToBeginPOSIXTime) +import Ledger.Typed.Scripts (validatorHash) +import Ledger.Value (Value, assetClass, singleton, unAssetClass, valueOf) +import Mlabs.EfficientNFT.Dao (daoValidator) +import Mlabs.EfficientNFT.Lock (lockValidator) +import Mlabs.EfficientNFT.Marketplace (marketplaceValidator) +import Mlabs.EfficientNFT.Token (mkTokenName, policy) +import Mlabs.EfficientNFT.Types ( + LockAct (Unstake), + LockDatum (LockDatum), + MarketplaceDatum (MarketplaceDatum), + MintAct (BurnToken, ChangeOwner, ChangePrice, MintToken), + NftCollection (NftCollection), + NftData (NftData), + NftId (NftId), + nftCollection'author, + nftCollection'authorShare, + nftCollection'collectionNftCs, + nftCollection'daoScript, + nftCollection'daoShare, + nftCollection'lockLockup, + nftCollection'lockLockupEnd, + nftCollection'lockingScript, + nftData'nftCollection, + nftData'nftId, + nftId'collectionNftTn, + nftId'owner, + nftId'price, + ) +import Plutus.Test.Model ( + BchConfig, + FakeCoin (FakeCoin), + Run, + addMintRedeemer, + bchConfig, + bchConfigSlotConfig, + currentSlot, + fakeCoin, + fakeValue, + filterSlot, + logError, + mintValue, + newUser, + payToPubKey, + payToScript, + payToScriptHash, + payWithDatumToPubKey, + scriptBoxAt, + sendTx, + signTx, + spend, + spendBox, + testLimits, + txBoxOut, + userSpend, + validateIn, + ) +import Plutus.V1.Ledger.Ada (getLovelace, lovelaceValueOf, toValue) +import Plutus.V1.Ledger.Api (toBuiltinData) +import PlutusTx.Enum (fromEnum, toEnum) +import PlutusTx.Prelude (divide) +import Test.Tasty (TestTree, testGroup) + +test :: BchConfig -> TestTree +test cfg = + testGroup + "Resources usage" + [ good "Seabug scripts" 3 seabugActions + ] + where + good msg n act = + testLimits + initFunds + cfg + msg + (filterSlot (> n)) + -- uncomment to see stats, it introduces fake error, script will fail but we can see the results + -- $ (>> logError "Show stats") + act + +cnftCoinA :: FakeCoin +cnftCoinA = FakeCoin "aa" + +initFunds :: Value +initFunds = mconcat [lovelaceValueOf 300_000_000, fakeValue cnftCoinA 1] + +seabugActions :: Run () +seabugActions = do + -- Use the same slot config as is used onchain + modify (\bch -> bch {bchConfig = (bchConfig bch) {bchConfigSlotConfig = def}}) + + w1 <- newUser $ lovelaceValueOf 100_000_000 <> fakeValue cnftCoinA 1 + w2 <- newUser $ lovelaceValueOf 100_000_000 + w3 <- newUser $ lovelaceValueOf 100_000_000 + + mint w1 [w3] cnftCoinA 10_000_000 + >>= changePrice 8_000_000 + >>= marketplaceDeposit + >>= marketplaceChangePrice 50_000_000 + >>= marketplaceBuy w2 + >>= marketplaceChangePrice 10_000_000 + >>= marketplaceRedeem + >>= unstake + feeWithdraw w3 [w3] + +feeWithdraw :: PubKeyHash -> [PubKeyHash] -> Run () +feeWithdraw pkh vaultKeys = do + boxes <- scriptBoxAt daoValidator' + let feeValue = foldMap (txOutValue . txBoxOut) boxes + void + . (sendTx <=< signTx pkh) + . mconcat + $ payToPubKey pkh feeValue : + fmap (spendBox daoValidator' (toBuiltinData ())) boxes + where + daoValidator' = daoValidator vaultKeys + +unstake :: NftData -> Run () +unstake nftData = do + box <- fromJust . find findCnft <$> scriptBoxAt lockValidator' + utxos <- spend owner (singleton nftCS nftTN 1) + now <- slotToBeginPOSIXTime def <$> currentSlot + void + . (sendTx <=< signTx owner <=< validateIn (range now)) + . addMintRedeemer policy' redeemer + . mconcat + $ [ spendBox lockValidator' (toBuiltinData $ Unstake (PaymentPubKeyHash owner) (nftId'price nft)) box + , mintValue policy' nftVal + , userSpend utxos + , payToPubKey owner $ singleton cnftCS cnftTN 1 <> toValue minAdaTxOut + ] + where + findCnft box = valueOf (txOutValue . txBoxOut $ box) cnftCS cnftTN == 1 + redeemer = BurnToken nft + policy' = policy collection + owner = unPaymentPubKeyHash . nftId'owner $ nft + nftCS = scriptCurrencySymbol policy' + nft = nftData'nftId nftData + collection = nftData'nftCollection nftData + nftTN = mkTokenName nft + nftVal = singleton nftCS nftTN (-1) + cnftCS = nftCollection'collectionNftCs collection + cnftTN = nftId'collectionNftTn nft + lockValidator' = lockValidator cnftCS 5 5 + range now = Interval (LowerBound (Finite now) True) (UpperBound PosInf False) + +marketplaceBuy :: PubKeyHash -> NftData -> Run NftData +marketplaceBuy newOwner nftData = do + box <- fromJust . find findNft <$> scriptBoxAt marketplaceValidator + utxos <- spend newOwner (lovelaceValueOf . fromEnum . nftId'price . nftData'nftId $ nftData) + void + . (sendTx <=< signTx newOwner) + . addMintRedeemer policy' redeemer + . mconcat + $ [ mintValue policy' (newNftVal <> oldNftVal) + , payToScript + marketplaceValidator + (toBuiltinData . MarketplaceDatum $ assetClass nftCS newNftTN) + (newNftVal <> toValue minAdaTxOut) + , spendBox marketplaceValidator (toBuiltinData ()) box + , payWithDatumToPubKey oldOwner datum (lovelaceValueOf ownerShare) + , userSpend utxos + ] + <> filterLowValue + authorShare + (payWithDatumToPubKey authorPkh datum (lovelaceValueOf authorShare)) + <> filterLowValue + daoShare + (payToScriptHash daoHash datum (lovelaceValueOf daoShare)) + pure $ NftData (nftData'nftCollection nftData) newNft + where + filterLowValue v t + | v < getLovelace minAdaTxOut = mempty + | otherwise = pure t + getShare share = (oldPrice * share) `divide` 10000 + authorShare :: Integer = getShare (fromEnum . nftCollection'authorShare . nftData'nftCollection $ nftData) + daoShare = getShare (fromEnum . nftCollection'daoShare . nftData'nftCollection $ nftData) + datum = toBuiltinData (nftCS, oldNftTN) + findNft box = valueOf (txOutValue . txBoxOut $ box) nftCS oldNftTN == 1 + redeemer = ChangeOwner oldNft (PaymentPubKeyHash newOwner) + policy' = policy (nftData'nftCollection nftData) + nftCS = scriptCurrencySymbol policy' + oldNft = nftData'nftId nftData + oldNftTN = mkTokenName oldNft + oldNftVal = singleton nftCS oldNftTN (-1) + newNft = oldNft {nftId'owner = PaymentPubKeyHash newOwner} + newNftTN = mkTokenName newNft + newNftVal = singleton nftCS newNftTN 1 + oldOwner = unPaymentPubKeyHash . nftId'owner $ oldNft + oldPrice = fromEnum . nftId'price $ oldNft + filterLow x + | x < getLovelace minAdaTxOut = 0 + | otherwise = x + ownerShare = oldPrice - filterLow daoShare - filterLow authorShare + authorPkh = unPaymentPubKeyHash . nftCollection'author . nftData'nftCollection $ nftData + daoHash = nftCollection'daoScript . nftData'nftCollection $ nftData + +marketplaceChangePrice :: Integer -> NftData -> Run NftData +marketplaceChangePrice newPrice nftData = do + box <- fromJust . find findNft <$> scriptBoxAt marketplaceValidator + void + . (sendTx <=< signTx owner) + . addMintRedeemer policy' redeemer + . mconcat + $ [ mintValue policy' (newNftVal <> oldNftVal) + , payToScript + marketplaceValidator + (toBuiltinData . MarketplaceDatum $ assetClass nftCS newNftTN) + (newNftVal <> toValue minAdaTxOut) + , spendBox marketplaceValidator (toBuiltinData ()) box + ] + pure $ NftData (nftData'nftCollection nftData) newNft + where + findNft box = valueOf (txOutValue . txBoxOut $ box) nftCS oldNftTN == 1 + redeemer = ChangePrice oldNft (toEnum newPrice) + policy' = policy (nftData'nftCollection nftData) + nftCS = scriptCurrencySymbol policy' + oldNft = nftData'nftId nftData + oldNftTN = mkTokenName oldNft + oldNftVal = singleton nftCS oldNftTN (-1) + newNft = oldNft {nftId'price = toEnum newPrice} + newNftTN = mkTokenName newNft + newNftVal = singleton nftCS newNftTN 1 + owner = unPaymentPubKeyHash . nftId'owner $ oldNft + +marketplaceRedeem :: NftData -> Run NftData +marketplaceRedeem nftData = do + box <- fromJust . find findNft <$> scriptBoxAt marketplaceValidator + void + . (sendTx <=< signTx owner) + . addMintRedeemer policy' redeemer + . mconcat + $ [ mintValue policy' (newNftVal <> oldNftVal) + , payToPubKey owner (newNftVal <> toValue minAdaTxOut) + , spendBox marketplaceValidator (toBuiltinData ()) box + ] + pure $ NftData (nftData'nftCollection nftData) newNft + where + findNft box = valueOf (txOutValue . txBoxOut $ box) nftCS oldNftTN == 1 + redeemer = ChangePrice oldNft (toEnum newPrice) + policy' = policy (nftData'nftCollection nftData) + nftCS = scriptCurrencySymbol policy' + oldNft = nftData'nftId nftData + oldNftTN = mkTokenName oldNft + oldNftVal = singleton nftCS oldNftTN (-1) + newNft = oldNft {nftId'price = toEnum newPrice} + newNftTN = mkTokenName newNft + newNftVal = singleton nftCS newNftTN 1 + owner = unPaymentPubKeyHash . nftId'owner $ oldNft + newPrice = subtract 1 . fromEnum . nftId'price $ oldNft + +marketplaceDeposit :: NftData -> Run NftData +marketplaceDeposit nftData = do + utxos <- spend owner (singleton nftCS nftTN 1 <> toValue minAdaTxOut) + void + . (sendTx <=< signTx owner) + . mconcat + $ [ payToScript + marketplaceValidator + (toBuiltinData . MarketplaceDatum $ assetClass nftCS nftTN) + (nftVal <> toValue minAdaTxOut) + , userSpend utxos + ] + pure nftData + where + policy' = policy (nftData'nftCollection nftData) + nftTN = mkTokenName . nftData'nftId $ nftData + nftCS = scriptCurrencySymbol policy' + nftVal = singleton nftCS nftTN 1 + owner = unPaymentPubKeyHash . nftId'owner . nftData'nftId $ nftData + +changePrice :: Integer -> NftData -> Run NftData +changePrice newPrice nftData = do + utxos <- spend owner (singleton nftCS oldNftTN 1 <> toValue minAdaTxOut) + void + . (sendTx <=< signTx owner) + . addMintRedeemer policy' redeemer + . mconcat + $ [ mintValue policy' (newNftVal <> oldNftVal) + , payToPubKey owner (newNftVal <> toValue minAdaTxOut) + , userSpend utxos + ] + pure $ NftData (nftData'nftCollection nftData) newNft + where + redeemer = ChangePrice oldNft (toEnum newPrice) + policy' = policy (nftData'nftCollection nftData) + nftCS = scriptCurrencySymbol policy' + oldNft = nftData'nftId nftData + oldNftTN = mkTokenName oldNft + oldNftVal = singleton nftCS oldNftTN (-1) + newNft = oldNft {nftId'price = toEnum newPrice} + newNftTN = mkTokenName newNft + newNftVal = singleton nftCS newNftTN 1 + owner = unPaymentPubKeyHash . nftId'owner . nftData'nftId $ nftData + +mint :: PubKeyHash -> [PubKeyHash] -> FakeCoin -> Integer -> Run NftData +mint pkh vaultKeys cnftCoin price = do + utxos <- spend pkh (cnftVal <> toValue minAdaTxOut) + void + . (sendTx <=< signTx pkh) + . addMintRedeemer policy' redeemer + . mconcat + $ [ mintValue policy' nftVal + , payToScript lockScript (toBuiltinData $ LockDatum nftCS 0 cnftTN) cnftVal + , payToPubKey pkh (nftVal <> toValue minAdaTxOut) + , userSpend utxos + ] + pure $ NftData collection nft + where + redeemer = MintToken nft + lockScript = lockValidator cnftCS 5 5 + daoHash = validatorHash $ daoValidator vaultKeys + collection = + NftCollection + { nftCollection'collectionNftCs = cnftCS + , nftCollection'lockLockup = 5 + , nftCollection'lockLockupEnd = 5 + , nftCollection'lockingScript = validatorHash lockScript + , nftCollection'author = PaymentPubKeyHash pkh + , nftCollection'authorShare = toEnum 0 + , nftCollection'daoScript = daoHash + , nftCollection'daoShare = toEnum 10_00 + } + policy' = policy collection + nft = NftId cnftTN (toEnum price) (PaymentPubKeyHash pkh) + nftTN = mkTokenName nft + nftCS = scriptCurrencySymbol policy' + nftVal = singleton nftCS nftTN 1 + cnftTN = snd . unAssetClass . fakeCoin $ cnftCoin + cnftCS = fst . unAssetClass . fakeCoin $ cnftCoin + cnftVal = fakeValue cnftCoinA 1 <> toValue minAdaTxOut diff --git a/mlabs/test/Test/EfficientNFT/Script/FeeWithdraw.hs b/mlabs/test/Test/EfficientNFT/Script/FeeWithdraw.hs new file mode 100644 index 000000000..2d6affae8 --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/FeeWithdraw.hs @@ -0,0 +1,61 @@ +module Test.EfficientNFT.Script.FeeWithdraw (test) where + +import Prelude + +import Ledger ( + CurrencySymbol, + minAdaTxOut, + unPaymentPubKeyHash, + ) +import Ledger.Value (CurrencySymbol (CurrencySymbol), Value, assetClassValue, singleton, unAssetClass) +import Plutus.V1.Ledger.Ada (lovelaceValueOf, toValue) +import Plutus.V1.Ledger.Api (toBuiltinData) +import PlutusTx.Prelude (BuiltinData) +import Test.Tasty (TestTree, localOption, testGroup) +import Test.Tasty.Plutus.Context (ContextBuilder, Purpose (ForSpending), mintsValue, paysToSelf, paysToWallet, signedWith) +import Test.Tasty.Plutus.Script.Unit (shouldValidate, shouldn'tValidate) +import Test.Tasty.Plutus.TestData (TestData (SpendingTest)) +import Test.Tasty.Plutus.WithScript (withTestScript) + +import Mlabs.EfficientNFT.Types +import Test.EfficientNFT.Script.Values (shouldFailWithErr) +import Test.EfficientNFT.Script.Values qualified as TestValues + +test :: TestTree +test = withTestScript "Fee withdraw" TestValues.testDaoScript $ do + shouldValidate "Valid withdraw - pkh1" validData validCtx1 + shouldValidate "Valid withdraw - pkh2" validData validCtx2 + shouldValidate "Valid withdraw - many signatures" validData manyPkhsCtx + shouldn'tValidate "Fail with wrong pkh" validData invalidPkhCtx + shouldn'tValidate "Fail with no signature" validData noPkhCtx + +dtm :: BuiltinData +dtm = toBuiltinData () + +redeemer :: BuiltinData +redeemer = toBuiltinData () + +validData :: TestData ( 'ForSpending BuiltinData BuiltinData) +validData = SpendingTest dtm redeemer val + where + val = lovelaceValueOf 5_000_000 + +validCtx1 :: ContextBuilder ( 'ForSpending BuiltinData BuiltinData) +validCtx1 = signedWith (unPaymentPubKeyHash TestValues.userOnePkh) + +validCtx2 :: ContextBuilder ( 'ForSpending BuiltinData BuiltinData) +validCtx2 = signedWith (unPaymentPubKeyHash TestValues.userTwoPkh) + +manyPkhsCtx :: ContextBuilder ( 'ForSpending BuiltinData BuiltinData) +manyPkhsCtx = + mconcat + [ signedWith (unPaymentPubKeyHash TestValues.userOnePkh) + , signedWith (unPaymentPubKeyHash TestValues.userTwoPkh) + , signedWith (unPaymentPubKeyHash TestValues.otherPkh) + ] + +invalidPkhCtx :: ContextBuilder ( 'ForSpending BuiltinData BuiltinData) +invalidPkhCtx = signedWith (unPaymentPubKeyHash TestValues.otherPkh) + +noPkhCtx :: ContextBuilder ( 'ForSpending BuiltinData BuiltinData) +noPkhCtx = mempty diff --git a/mlabs/test/Test/EfficientNFT/Script/TokenBurn.hs b/mlabs/test/Test/EfficientNFT/Script/TokenBurn.hs new file mode 100644 index 000000000..687f0215d --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/TokenBurn.hs @@ -0,0 +1,116 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Test.EfficientNFT.Script.TokenBurn (test) where + +import Prelude qualified as Hask + +import Data.Data (Typeable) +import Data.List.NonEmpty (NonEmpty) +import Data.String (String) +import Ledger (PaymentPubKeyHash (unPaymentPubKeyHash), minAdaTxOut) +import Ledger.Value (CurrencySymbol, TokenName (TokenName, unTokenName)) +import Ledger.Value qualified as Value +import Plutus.V1.Ledger.Ada (toValue) +import PlutusTx.Positive (Positive, positive) +import PlutusTx.Prelude hiding (elem, (<>)) +import Test.Tasty (TestTree, localOption) +import Test.Tasty.Plutus.Context ( + ContextBuilder, + Purpose (ForMinting), + makeIncompleteContexts, + mintsValue, + paysToOther, + paysToPubKey, + paysToPubKeyWithDatum, + signedWith, + spendsFromOther, + spendsFromPubKey, + ) +import Test.Tasty.Plutus.Options (TestCurrencySymbol (TestCurrencySymbol)) +import Test.Tasty.Plutus.Script.Property (scriptPropertyFail) +import Test.Tasty.Plutus.Script.Unit (shouldValidate, shouldn'tValidate, shouldn'tValidateTracing) +import Test.Tasty.Plutus.TestData ( + Generator (GenForMinting), + MintingPolicyTask, + Outcome, + TestData (MintingTest), + TestItems (ItemsForMinting, mpCB, mpOutcome, mpRedeemer, mpTasks), + Tokens (Tokens), + burnTokens, + fromArbitrary, + mintTokens, + passIf, + ) +import Test.Tasty.Plutus.WithScript (WithScript, withTestScript) +import Prelude (elem, (<>)) + +import Mlabs.EfficientNFT.Token (mkTokenName) +import Mlabs.EfficientNFT.Types (MintAct (BurnToken, ChangeOwner)) +import Test.EfficientNFT.Script.Values (shouldFailWithErr) +import Test.EfficientNFT.Script.Values qualified as TestValues + +test :: TestTree +test = + withTestScript "Burn token" TestValues.testTokenPolicy $ do + shouldValidate "Valid burn" validData validCtx + + shouldFailWithErr + "Fail when minting other Sg" + "NFT must be burned" + additionalMintData + validCtx + + shouldFailWithErr + "Fail when no owner signature" + "Owner must sign the transaction" + validData + noSignCtx + + shouldFailWithErr + "Fail when not burning Sg" + "NFT must be burned" + noBurnData + validCtx + + shouldFailWithErr + "Fail when not unlocking CNFT" + "Underlying NFT must be unlocked" + validData + noUnlockCtx + +validData :: TestData ( 'ForMinting MintAct) +validData = + MintingTest + redeemer + (burnTokens (Tokens TestValues.tokenName [positive| 1 |])) + where + redeemer = BurnToken TestValues.nft1 + +additionalMintData = + MintingTest + redeemer + (burnTokens (Tokens TestValues.tokenName [positive| 1 |]) <> mintTokens (Tokens "foo" [positive| 1 |])) + where + redeemer = BurnToken TestValues.nft1 + +noBurnData = + MintingTest + redeemer + (mintTokens (Tokens "foo" [positive| 1 |])) + where + redeemer = BurnToken TestValues.nft1 + +validCtx :: ContextBuilder ( 'ForMinting MintAct) +validCtx = + noSignCtx + <> signedWith (unPaymentPubKeyHash TestValues.authorPkh) + +noSignCtx :: ContextBuilder ( 'ForMinting MintAct) +noSignCtx = + spendsFromOther TestValues.burnHash cnft () + <> paysToPubKey (unPaymentPubKeyHash TestValues.authorPkh) cnft + where + cnft = Value.assetClassValue TestValues.collectionNft 1 + +noUnlockCtx :: ContextBuilder ( 'ForMinting MintAct) +noUnlockCtx = signedWith (unPaymentPubKeyHash TestValues.authorPkh) diff --git a/mlabs/test/Test/EfficientNFT/Script/TokenChangeOwner.hs b/mlabs/test/Test/EfficientNFT/Script/TokenChangeOwner.hs new file mode 100644 index 000000000..9bc145a8c --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/TokenChangeOwner.hs @@ -0,0 +1,195 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Test.EfficientNFT.Script.TokenChangeOwner (test) where + +import Ledger (PaymentPubKeyHash (unPaymentPubKeyHash)) +import Ledger.Value (CurrencySymbol, TokenName (TokenName, unTokenName)) +import Ledger.Value qualified as Value +import PlutusTx.Positive (Positive, positive) + +import Data.Data (Typeable) +import Data.List.NonEmpty (NonEmpty) +import Data.String (String) +import PlutusTx.Prelude hiding (elem, (<>)) +import Prelude (elem, (<>)) + +import Test.Tasty (TestTree, localOption) +import Test.Tasty.Plutus.Context ( + ContextBuilder, + Purpose (ForMinting), + makeIncompleteContexts, + mintsValue, + paysToOther, + paysToPubKeyWithDatum, + ) +import Test.Tasty.Plutus.Options (TestCurrencySymbol (TestCurrencySymbol)) +import Test.Tasty.Plutus.Script.Property (scriptPropertyFail) +import Test.Tasty.Plutus.Script.Unit (shouldValidate, shouldn'tValidateTracing) +import Test.Tasty.Plutus.TestData ( + Generator (GenForMinting), + MintingPolicyTask, + Outcome, + TestData (MintingTest), + TestItems (ItemsForMinting, mpCB, mpOutcome, mpRedeemer, mpTasks), + Tokens (Tokens), + burnTokens, + fromArbitrary, + mintTokens, + passIf, + ) +import Test.Tasty.Plutus.WithScript (WithScript, withTestScript) + +import Mlabs.EfficientNFT.Token (mkTokenName) +import Mlabs.EfficientNFT.Types (MintAct (ChangeOwner), hash) + +import Test.EfficientNFT.Script.Values qualified as TestValues + +test :: TestTree +test = + localOption (TestCurrencySymbol testTokenCurSym) $ + withTestScript "Token change owner" TestValues.testTokenPolicy $ + do + shouldValidate "valid buy" validData validCtx + shouldFailWithErr + "Fail if token has wrong name" + "Exactly one new token must be minted and exactly one old burnt" + badTokenNameData + validCtx + + shouldFailWithErr + "Fail if old token is not burnt" + "Exactly one new token must be minted and exactly one old burnt" + oldTokenNotBurntData + validCtx + + shouldFailWithErr + "Fail if new token is not minted" + "Exactly one new token must be minted and exactly one old burnt" + newTokenNotMintedData + validCtx + + scriptPropertyFail "Should mint and burn exactly 1 token" $ + GenForMinting fromArbitrary oneTokenMintAndBurn + + shouldValidate + "Pass if additional tokens (non-NFT) minted" + validData + manyTokensCtx + + mapM_ + (\(ctx, str) -> shouldFailWithErr str "Royalities not paid" validData ctx) + insufficientShareCtxs + +validData :: TestData ( 'ForMinting MintAct) +validData = MintingTest redeemer tasks + where + tasks = + burnTokens (Tokens validOldTokenName [positive| 1 |]) + <> mintTokens (Tokens validNewTokenName [positive| 1 |]) + redeemer = ChangeOwner TestValues.nft2 TestValues.userTwoPkh + +badTokenNameData :: TestData ( 'ForMinting MintAct) +badTokenNameData = MintingTest redeemer tasks + where + breakName = TokenName . hash . unTokenName + tasks = + burnTokens (Tokens validOldTokenName [positive| 1 |]) + <> mintTokens (Tokens (breakName validNewTokenName) [positive| 1 |]) + redeemer = ChangeOwner TestValues.nft2 TestValues.userTwoPkh + +oldTokenNotBurntData :: TestData ( 'ForMinting MintAct) +oldTokenNotBurntData = MintingTest redeemer tasks + where + tasks = mintTokens (Tokens validNewTokenName [positive| 1 |]) + redeemer = ChangeOwner TestValues.nft2 TestValues.userTwoPkh + +newTokenNotMintedData :: TestData ( 'ForMinting MintAct) +newTokenNotMintedData = MintingTest redeemer tasks + where + tasks = burnTokens (Tokens validOldTokenName [positive| 1 |]) + redeemer = ChangeOwner TestValues.nft2 TestValues.userTwoPkh + +validOldTokenName :: TokenName +validOldTokenName = mkTokenName TestValues.nft2 + +validNewTokenName :: TokenName +validNewTokenName = mkTokenName TestValues.nft3 + +daoShareCtx :: ContextBuilder ( 'ForMinting MintAct) +daoShareCtx = + paysToOther + TestValues.daoValHash + TestValues.daoShareVal + (testTokenCurSym, validOldTokenName) + +ownerShareCtx :: ContextBuilder ( 'ForMinting MintAct) +ownerShareCtx = + paysToPubKeyWithDatum + (unPaymentPubKeyHash TestValues.userOnePkh) + TestValues.ownerShareVal + (testTokenCurSym, validOldTokenName) + +authorShareCtx :: ContextBuilder ( 'ForMinting MintAct) +authorShareCtx = + paysToPubKeyWithDatum + (unPaymentPubKeyHash TestValues.authorPkh) + TestValues.authorShareVal + (testTokenCurSym, validOldTokenName) + +validCtx :: ContextBuilder ( 'ForMinting MintAct) +validCtx = daoShareCtx <> authorShareCtx <> ownerShareCtx + +insufficientShareCtxs :: [(ContextBuilder ( 'ForMinting MintAct), String)] +insufficientShareCtxs = + makeIncompleteContexts + [ (daoShareCtx, "Fails when marketplace share is insufficient") + , (authorShareCtx, "Fails when author share is insufficient") + , (ownerShareCtx, "Fails when owner share is insufficient") + ] + +manyTokensCtx :: ContextBuilder ( 'ForMinting MintAct) +manyTokensCtx = + validCtx + <> mintsValue additionalValue + where + additionalValue = Value.singleton (Value.CurrencySymbol "aa") (TokenName "ff") 1 + +-- | Creates TestItems with an arbitrary key used in Redeemer +oneTokenMintAndBurn :: (Positive, Positive) -> TestItems ( 'ForMinting MintAct) +oneTokenMintAndBurn (mintAmt, burnAmt) = + ItemsForMinting + { mpRedeemer = redeemer + , mpTasks = tasks + , mpCB = validCtx + , mpOutcome = out + } + where + mintAmt' = mintAmt + [positive| 1 |] + burnAmt' = burnAmt + [positive| 1 |] + + toksMint = Tokens validNewTokenName mintAmt' + toksBurn = Tokens validOldTokenName burnAmt' + + redeemer = ChangeOwner TestValues.nft2 TestValues.userTwoPkh + + tasks :: NonEmpty MintingPolicyTask + tasks = mintTokens toksMint <> burnTokens toksBurn + + out :: Outcome + out = passIf $ fromEnum mintAmt' == 1 && fromEnum burnAmt' == 1 + +testTokenCurSym :: CurrencySymbol +testTokenCurSym = "aabbcc" + +shouldFailWithErr :: + forall (p :: Purpose). + Typeable p => + String -> + BuiltinString -> + TestData p -> + ContextBuilder p -> + WithScript p () +shouldFailWithErr name errMsg = + shouldn'tValidateTracing name (errMsg' `elem`) + where + errMsg' = fromBuiltin errMsg diff --git a/mlabs/test/Test/EfficientNFT/Script/TokenChangePrice.hs b/mlabs/test/Test/EfficientNFT/Script/TokenChangePrice.hs new file mode 100644 index 000000000..ccc78adb9 --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/TokenChangePrice.hs @@ -0,0 +1,172 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Test.EfficientNFT.Script.TokenChangePrice (test) where + +import Ledger (PaymentPubKeyHash (unPaymentPubKeyHash)) +import Ledger.Value (TokenName (TokenName, unTokenName)) +import Mlabs.EfficientNFT.Token ( + mkPolicy, + ) +import Mlabs.EfficientNFT.Types ( + MintAct (ChangePrice), + NftId (nftId'price), + hash, + ) +import Plutus.V1.Ledger.Ada qualified as Value +import PlutusTx qualified +import PlutusTx.Positive (positive) +import PlutusTx.Prelude hiding (elem, mconcat, mempty, (<>)) +import Test.EfficientNFT.Script.Values qualified as TestValues +import Test.Tasty (TestTree) +import Test.Tasty.Plutus.Context ( + ContextBuilder, + Purpose (ForMinting), + input, + signedWith, + spendsFromPubKey, + spendsFromPubKeySigned, + ) +import Test.Tasty.Plutus.Script.Unit ( + shouldValidate, + shouldn'tValidateTracing, + ) +import Test.Tasty.Plutus.TestData (TestData (MintingTest), Tokens (Tokens), burnTokens, mintTokens) +import Test.Tasty.Plutus.TestScript (TestScript, mkTestMintingPolicy, toTestMintingPolicy) +import Test.Tasty.Plutus.WithScript ( + WithScript, + withTestScript, + ) +import Type.Reflection (Typeable) +import Prelude (String, elem, (<>)) + +test :: TestTree +test = + withTestScript "Change price" TestValues.testTokenPolicy $ do + shouldValidate "Change price with valid data and context" validData validCtx + + shouldFailWithErr + "Fail if not signed by owner" + "Owner must sign the transaction" + validData + wrongSignCtx + + shouldFailWithErr + "Fail if new token not minted" + "Exactly one new token must be minted and exactly one old burnt" + newNotMintedData + validCtx + + shouldFailWithErr + "Fail if old token not burnt" + "Exactly one new token must be minted and exactly one old burnt" + oldNotBurntData + validCtx + + shouldFailWithErr + "Fail if wrong amount minted" + "Exactly one new token must be minted and exactly one old burnt" + wrongAmtMintedData + validCtx + + shouldFailWithErr + "Fail if wrong amount burned" + "Exactly one new token must be minted and exactly one old burnt" + wrongAmtBurnedData + validCtx + + shouldFailWithErr + "Fail if token with wrong name minted" + "Exactly one new token must be minted and exactly one old burnt" + wrongNameMintedData + validCtx + + shouldFailWithErr + "Fail if token with wrong name burned" + "Exactly one new token must be minted and exactly one old burnt" + wrongNameBurnedData + validCtx + +-- test data +testRedeemer :: MintAct +testRedeemer = ChangePrice TestValues.nft1 newPrice + where + newPrice = nftId'price TestValues.newPriceNft1 + +validData :: TestData ( 'ForMinting MintAct) +validData = + MintingTest + testRedeemer + ( burnTokens (Tokens TestValues.tokenName [positive| 1 |]) + <> mintTokens (Tokens TestValues.newPriceTokenName [positive| 1 |]) + ) + +newNotMintedData :: TestData ( 'ForMinting MintAct) +newNotMintedData = + MintingTest + testRedeemer + (burnTokens (Tokens TestValues.tokenName [positive| 1 |])) + +oldNotBurntData :: TestData ( 'ForMinting MintAct) +oldNotBurntData = + MintingTest + testRedeemer + (burnTokens (Tokens TestValues.newPriceTokenName [positive| 1 |])) + +wrongAmtMintedData :: TestData ( 'ForMinting MintAct) +wrongAmtMintedData = + MintingTest + testRedeemer + ( burnTokens (Tokens TestValues.tokenName [positive| 1 |]) + <> mintTokens (Tokens TestValues.newPriceTokenName [positive| 2 |]) + ) + +wrongAmtBurnedData :: TestData ( 'ForMinting MintAct) +wrongAmtBurnedData = + MintingTest + testRedeemer + ( mintTokens (Tokens TestValues.tokenName [positive| 1 |]) + <> mintTokens (Tokens TestValues.newPriceTokenName [positive| 1 |]) + ) + +-- test context +validCtx :: ContextBuilder ( 'ForMinting r) +validCtx = + let pkh = unPaymentPubKeyHash TestValues.authorPkh + in spendsFromPubKeySigned pkh (Value.lovelaceValueOf 1000000) + +wrongNameMintedData :: TestData ( 'ForMinting MintAct) +wrongNameMintedData = + MintingTest + testRedeemer + ( burnTokens (Tokens TestValues.tokenName [positive| 1 |]) + <> mintTokens (Tokens (breakName TestValues.newPriceTokenName) [positive| 1 |]) + ) + +wrongNameBurnedData :: TestData ( 'ForMinting MintAct) +wrongNameBurnedData = + MintingTest + testRedeemer + ( burnTokens (Tokens (breakName TestValues.tokenName) [positive| 1 |]) + <> mintTokens (Tokens TestValues.newPriceTokenName [positive| 1 |]) + ) + +wrongSignCtx :: ContextBuilder ( 'ForMinting r) +wrongSignCtx = + spendsFromPubKey (unPaymentPubKeyHash TestValues.authorPkh) (Value.lovelaceValueOf 1000000) + <> signedWith (unPaymentPubKeyHash TestValues.otherPkh) + +shouldFailWithErr :: + forall (p :: Purpose). + Typeable p => + String -> + BuiltinString -> + TestData p -> + ContextBuilder p -> + WithScript p () +shouldFailWithErr name errMsg = + shouldn'tValidateTracing name (errMsg' `elem`) + where + errMsg' = fromBuiltin errMsg + +breakName :: TokenName -> TokenName +breakName = TokenName . hash . unTokenName diff --git a/mlabs/test/Test/EfficientNFT/Script/TokenMarketplaceBuy.hs b/mlabs/test/Test/EfficientNFT/Script/TokenMarketplaceBuy.hs new file mode 100644 index 000000000..c0aa3cad1 --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/TokenMarketplaceBuy.hs @@ -0,0 +1,104 @@ +module Test.EfficientNFT.Script.TokenMarketplaceBuy (test) where + +import Prelude + +import Ledger (CurrencySymbol, minAdaTxOut, unPaymentPubKeyHash) +import Ledger.Value (CurrencySymbol (CurrencySymbol), TokenName (TokenName, unTokenName), assetClass, singleton) +import Plutus.V1.Ledger.Ada (toValue) +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import PlutusTx.Builtins (BuiltinData) +import Test.Tasty (TestTree) +import Test.Tasty.Plutus.Context (ContextBuilder, Purpose (ForSpending), mintsValue, paysToOther, paysToPubKeyWithDatum, paysToSelf) +import Test.Tasty.Plutus.Script.Unit (shouldValidate, shouldn'tValidate) +import Test.Tasty.Plutus.TestData (TestData (SpendingTest)) +import Test.Tasty.Plutus.WithScript (withTestScript) + +import Mlabs.EfficientNFT.Token (mkTokenName) +import Mlabs.EfficientNFT.Types (MarketplaceDatum (MarketplaceDatum)) +import Test.EfficientNFT.Script.Values qualified as TestValues + +test :: TestTree +test = withTestScript "Buy" TestValues.testMarketplaceScript $ do + shouldValidate "Buy with valid data and context" validData validCtx + + shouldn'tValidate "Fail when not minting" validData noMintCtx + +dtm :: MarketplaceDatum +dtm = MarketplaceDatum $ assetClass mockSgCs validOldTokenName + +redeemer :: BuiltinData +redeemer = toBuiltinData () + +mockSgCs :: CurrencySymbol +mockSgCs = CurrencySymbol "ff" + +secondCs :: CurrencySymbol +secondCs = CurrencySymbol "aa" + +secondTn :: TokenName +secondTn = TokenName "foo" + +validOldTokenName :: TokenName +validOldTokenName = mkTokenName TestValues.nft2 + +validNewTokenName :: TokenName +validNewTokenName = mkTokenName TestValues.nft3 + +validData :: TestData ( 'ForSpending MarketplaceDatum BuiltinData) +validData = SpendingTest dtm redeemer val + where + val = + mconcat + [ toValue minAdaTxOut + , singleton mockSgCs validOldTokenName 1 + ] + +validCtx :: ContextBuilder ( 'ForSpending MarketplaceDatum BuiltinData) +validCtx = + mconcat + [ mintsValue $ + mconcat + [ singleton mockSgCs validOldTokenName (negate 1) + , singleton mockSgCs validNewTokenName 1 + ] + , paysToSelf + ( mconcat + [ singleton mockSgCs validNewTokenName 1 + , toValue minAdaTxOut + ] + ) + dtm + , royalitiesCtx + ] + +noMintCtx :: ContextBuilder ( 'ForSpending MarketplaceDatum BuiltinData) +noMintCtx = + mconcat + [ paysToSelf + ( mconcat + [ singleton mockSgCs validNewTokenName 1 + , singleton mockSgCs (TokenName (unTokenName secondTn <> "x")) 1 + , singleton secondCs TestValues.tokenName 1 + , toValue minAdaTxOut + ] + ) + dtm + , royalitiesCtx + ] + +royalitiesCtx :: ContextBuilder p +royalitiesCtx = + mconcat + [ paysToOther + TestValues.daoValHash + TestValues.daoShareVal + (mockSgCs, validOldTokenName) + , paysToPubKeyWithDatum + (unPaymentPubKeyHash TestValues.userOnePkh) + TestValues.ownerShareVal + (mockSgCs, validOldTokenName) + , paysToPubKeyWithDatum + (unPaymentPubKeyHash TestValues.authorPkh) + TestValues.authorShareVal + (mockSgCs, validOldTokenName) + ] diff --git a/mlabs/test/Test/EfficientNFT/Script/TokenMarketplaceRedeem.hs b/mlabs/test/Test/EfficientNFT/Script/TokenMarketplaceRedeem.hs new file mode 100644 index 000000000..d11a50930 --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/TokenMarketplaceRedeem.hs @@ -0,0 +1,86 @@ +module Test.EfficientNFT.Script.TokenMarketplaceRedeem (test) where + +import Prelude + +import Ledger (CurrencySymbol, minAdaTxOut, unPaymentPubKeyHash) +import Ledger.Value (CurrencySymbol (CurrencySymbol), TokenName (TokenName), assetClass, singleton, unTokenName) +import Plutus.V1.Ledger.Ada (toValue) +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import PlutusTx.Builtins (BuiltinData) +import Test.Tasty (TestTree) +import Test.Tasty.Plutus.Context (ContextBuilder, Purpose (ForSpending), mintsValue, paysToPubKey, signedWith) +import Test.Tasty.Plutus.Script.Unit (shouldValidate, shouldn'tValidate) +import Test.Tasty.Plutus.TestData (TestData (SpendingTest)) +import Test.Tasty.Plutus.WithScript (withTestScript) + +import Mlabs.EfficientNFT.Types (MarketplaceDatum (MarketplaceDatum)) +import Test.EfficientNFT.Script.Values qualified as TestValues + +test :: TestTree +test = withTestScript "Redeem" TestValues.testMarketplaceScript $ do + shouldValidate "Redeem with valid data and context" validData validCtx + + shouldn'tValidate "Fail when not minting" validData noMintCtx + +dtm :: MarketplaceDatum +dtm = MarketplaceDatum $ assetClass mockSgCs TestValues.tokenName + +redeemer :: BuiltinData +redeemer = toBuiltinData () + +mockSgCs :: CurrencySymbol +mockSgCs = CurrencySymbol "ff" + +secondCs :: CurrencySymbol +secondCs = CurrencySymbol "aa" + +secondTn :: TokenName +secondTn = TokenName "foo" + +validData :: TestData ( 'ForSpending MarketplaceDatum BuiltinData) +validData = SpendingTest dtm redeemer val + where + val = + mconcat + [ toValue minAdaTxOut + , singleton mockSgCs TestValues.tokenName 1 + ] + +validCtx :: ContextBuilder ( 'ForSpending MarketplaceDatum BuiltinData) +validCtx = + mconcat + [ mintsValue $ + mconcat + [ singleton mockSgCs TestValues.tokenName (negate 1) + , singleton mockSgCs TestValues.newPriceTokenName 1 + ] + , paysToPubKey + (unPaymentPubKeyHash TestValues.authorPkh) + ( mconcat + [ singleton mockSgCs TestValues.newPriceTokenName 1 + , toValue minAdaTxOut + ] + ) + , signedWith (unPaymentPubKeyHash TestValues.authorPkh) + ] + +noMintCtx :: ContextBuilder ( 'ForSpending MarketplaceDatum BuiltinData) +noMintCtx = + mconcat + [ mintsValue $ + mconcat + [ singleton mockSgCs TestValues.newPriceTokenName 1 + , toValue minAdaTxOut + , singleton mockSgCs secondTn (negate 1) + , singleton mockSgCs (TokenName (unTokenName secondTn <> "x")) 1 + ] + , paysToPubKey + (unPaymentPubKeyHash TestValues.authorPkh) + ( mconcat + [ singleton mockSgCs TestValues.newPriceTokenName 1 + , singleton mockSgCs (TokenName (unTokenName secondTn <> "x")) 1 + , singleton secondCs TestValues.tokenName 1 + , toValue minAdaTxOut + ] + ) + ] diff --git a/mlabs/test/Test/EfficientNFT/Script/TokenMarketplaceSetPrice.hs b/mlabs/test/Test/EfficientNFT/Script/TokenMarketplaceSetPrice.hs new file mode 100644 index 000000000..52e204aec --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/TokenMarketplaceSetPrice.hs @@ -0,0 +1,78 @@ +module Test.EfficientNFT.Script.TokenMarketplaceSetPrice (test) where + +import Prelude + +import Ledger (CurrencySymbol, minAdaTxOut) +import Ledger.Value (CurrencySymbol (CurrencySymbol), TokenName (TokenName), assetClass, singleton, unTokenName) +import Plutus.V1.Ledger.Ada (toValue) +import Plutus.V1.Ledger.Api (ToData (toBuiltinData)) +import PlutusTx.Builtins (BuiltinData) +import Test.Tasty (TestTree) +import Test.Tasty.Plutus.Context (ContextBuilder, Purpose (ForSpending), mintsValue, paysToSelf) +import Test.Tasty.Plutus.Script.Unit (shouldValidate, shouldn'tValidate) +import Test.Tasty.Plutus.TestData (TestData (SpendingTest)) +import Test.Tasty.Plutus.WithScript (withTestScript) + +import Mlabs.EfficientNFT.Types (MarketplaceDatum (MarketplaceDatum)) +import Test.EfficientNFT.Script.Values qualified as TestValues + +test :: TestTree +test = withTestScript "Change price" TestValues.testMarketplaceScript $ do + shouldValidate "Change price with valid data and context" validData validCtx + + shouldn'tValidate "Fail when not minting" validData noMintCtx + +dtm :: MarketplaceDatum +dtm = MarketplaceDatum $ assetClass mockSgCs TestValues.tokenName + +redeemer :: BuiltinData +redeemer = toBuiltinData () + +mockSgCs :: CurrencySymbol +mockSgCs = CurrencySymbol "ff" + +secondCs :: CurrencySymbol +secondCs = CurrencySymbol "aa" + +secondTn :: TokenName +secondTn = TokenName "foo" + +validData :: TestData ( 'ForSpending MarketplaceDatum BuiltinData) +validData = SpendingTest dtm redeemer val + where + val = + mconcat + [ toValue minAdaTxOut + , singleton mockSgCs TestValues.tokenName 1 + ] + +validCtx :: ContextBuilder ( 'ForSpending MarketplaceDatum BuiltinData) +validCtx = + mconcat + [ mintsValue $ + mconcat + [ singleton mockSgCs TestValues.tokenName (negate 1) + , singleton mockSgCs TestValues.newPriceTokenName 1 + ] + , paysToSelf + ( mconcat + [ singleton mockSgCs TestValues.newPriceTokenName 1 + , toValue minAdaTxOut + ] + ) + dtm + ] + +noMintCtx :: ContextBuilder ( 'ForSpending MarketplaceDatum BuiltinData) +noMintCtx = + mconcat + [ paysToSelf + ( mconcat + [ singleton mockSgCs TestValues.newPriceTokenName 1 + , singleton mockSgCs (TokenName (unTokenName secondTn <> "x")) 1 + , singleton secondCs TestValues.tokenName 1 + , toValue minAdaTxOut + ] + ) + dtm + ] diff --git a/mlabs/test/Test/EfficientNFT/Script/TokenMint.hs b/mlabs/test/Test/EfficientNFT/Script/TokenMint.hs new file mode 100644 index 000000000..e490c8cbb --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/TokenMint.hs @@ -0,0 +1,112 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Test.EfficientNFT.Script.TokenMint (test) where + +import Data.Data (Typeable) +import Data.String (String) +import Ledger ( + TxOutRef (txOutRefId), + unPaymentPubKeyHash, + ) +import Ledger.Value (TokenName (TokenName), unTokenName) +import Plutus.V1.Ledger.Ada qualified as Ada +import Plutus.V1.Ledger.Value qualified as Value +import PlutusTx qualified +import PlutusTx.AssocMap qualified as Map +import PlutusTx.Positive (positive) +import PlutusTx.Prelude hiding (elem, mconcat, pure, (<>)) +import Test.Tasty (TestTree, localOption, testGroup) +import Test.Tasty.Plutus.Context ( + ContextBuilder, + Purpose (ForMinting), + mintsValue, + paysToOther, + paysToPubKey, + spendsFromPubKey, + ) +import Test.Tasty.Plutus.Options (TestTxId (TestTxId)) +import Test.Tasty.Plutus.Script.Unit ( + shouldValidate, + shouldn'tValidateTracing, + ) +import Test.Tasty.Plutus.TestData ( + TestData (MintingTest), + Tokens (Tokens), + mintTokens, + ) +import Test.Tasty.Plutus.TestScript (TestScript, mkTestMintingPolicy, toTestMintingPolicy) +import Test.Tasty.Plutus.WithScript (WithScript, withTestScript) +import Prelude (elem, mconcat, pure, (<>)) + +import Mlabs.EfficientNFT.Token (mkPolicy) +import Mlabs.EfficientNFT.Types (MintAct (MintToken), NftCollection (..), hash) +import Test.EfficientNFT.Script.Values (shouldFailWithErr) +import Test.EfficientNFT.Script.Values qualified as TestValues + +test :: TestTree +test = + localOption (TestTxId $ txOutRefId TestValues.mintTxOutRef) $ + withTestScript "Mint" TestValues.testTokenPolicy $ do + shouldValidate "Valid data and context" validData validCtx + + shouldFailWithErr + "Fail if token has wrong name" + "Exactly one NFT must be minted" + badTokenNameData + validCtx + + shouldFailWithErr + "Fail if minted amount not 1" + "Exactly one NFT must be minted" + wrongNftQuantityData + validCtx + + shouldValidate + "Pass if additional tokens (non-NFT) minted" + validData + manyTokensCtx + +-- test data +correctTokens :: Tokens +correctTokens = Tokens TestValues.tokenName [positive| 1 |] + +validData :: TestData ( 'ForMinting MintAct) +validData = + MintingTest + redeemer + (mintTokens (Tokens TestValues.tokenName [positive| 1 |])) + where + redeemer = MintToken TestValues.nft1 + +wrongNftQuantityData :: TestData ( 'ForMinting MintAct) +wrongNftQuantityData = + MintingTest + redeemer + (mintTokens (Tokens TestValues.tokenName [positive| 2 |])) + where + redeemer = MintToken TestValues.nft1 + +badTokenNameData :: TestData ( 'ForMinting MintAct) +badTokenNameData = + MintingTest + redeemer + (mintTokens badTokens) + where + breakName = TokenName . hash . unTokenName + badTokens = Tokens (breakName TestValues.tokenName) [positive| 1 |] + redeemer = MintToken TestValues.nft1 + +-- test context +validCtx :: ContextBuilder ( 'ForMinting MintAct) +validCtx = + mconcat + [ spendsFromPubKey (unPaymentPubKeyHash TestValues.authorPkh) (Ada.lovelaceValueOf 1000000) + , paysToOther TestValues.burnHash (Value.assetClassValue TestValues.collectionNft 1) () + ] + +manyTokensCtx :: ContextBuilder ( 'ForMinting MintAct) +manyTokensCtx = + validCtx + <> mintsValue additionalValue + where + additionalValue = Value.singleton (Value.CurrencySymbol "aa") (TokenName "ff") 1 diff --git a/mlabs/test/Test/EfficientNFT/Script/TokenRestake.hs b/mlabs/test/Test/EfficientNFT/Script/TokenRestake.hs new file mode 100644 index 000000000..e4fec9e7b --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/TokenRestake.hs @@ -0,0 +1,205 @@ +module Test.EfficientNFT.Script.TokenRestake (test) where + +import Prelude ((<>)) + +import Ledger ( + CurrencySymbol, + Slot (Slot), + minAdaTxOut, + unPaymentPubKeyHash, + ) +import Ledger.Value ( + CurrencySymbol (CurrencySymbol), + Value, + assetClassValue, + singleton, + unAssetClass, + ) +import Plutus.V1.Ledger.Ada (toValue) +import PlutusTx.Prelude hiding (elem, mempty, (<>)) +import Test.Tasty (TestTree, localOption, testGroup) +import Test.Tasty.Plutus.Context ( + ContextBuilder, + Purpose (ForSpending), + mintsValue, + paysToPubKey, + paysToSelf, + signedWith, + ) +import Test.Tasty.Plutus.Script.Unit (shouldValidate) +import Test.Tasty.Plutus.TestData (TestData (SpendingTest)) +import Test.Tasty.Plutus.WithScript (withTestScript) + +import Mlabs.EfficientNFT.Token (mkTokenName) +import Mlabs.EfficientNFT.Types +import Test.EfficientNFT.Script.Values (shouldFailWithErr) +import Test.EfficientNFT.Script.Values qualified as TestValues + +test :: TestTree +test = + testGroup + "Restaking" + [ localOption TestValues.afterDeadline $ + withTestScript "Restake CNFT after lockupEnd" TestValues.testLockScript $ do + shouldValidate "Pass with valid data and context" validData validCtx + + shouldFailWithErr + "Fail when lockup is extended" + "Current slot smaller than lockup+entered" + extendedData + validCtx + + shouldValidate + "Pass when minting non-sg tokens" + validData + mintOtherCtx + + sharedAlwaysInvalid + , localOption TestValues.afterDeadlineAndLockup $ + withTestScript "Restake CNFT after lockupEnd + lockup" TestValues.testLockScript $ do + shouldValidate "Pass with valid data and context" validData validCtx + + shouldValidate + "Pass when lockup is extended" + extendedData + extendedCtx + + shouldValidate + "Pass when minting non-sg tokens" + validData + mintOtherCtx + + sharedAlwaysInvalid + , localOption TestValues.beforeDeadline $ + withTestScript "Restake CNFT before lockupEnd" TestValues.testLockScript $ do + shouldFailWithErr + "Fail with valid data and context" + "Current slot smaller than lockupEnd" + validData + validCtx + + sharedAlwaysInvalid + ] + where + sharedAlwaysInvalid = do + shouldFailWithErr + "Fail when changing CO value" + "Values in CO cannot change" + validData + spendCOCtx + + shouldFailWithErr + "Fail when minting Sg" + "Cannot mint sg" + validData + mintSgCtx + + shouldFailWithErr + "Fail when owner didn't sign" + "Owner must sign the transaction" + validData + noOwnerSignatureCtx + + shouldFailWithErr + "Fail when no sg in input" + "Input does not contain sg" + validData + noSgCtx + + shouldFailWithErr + "Fail with invalid pkh data" + "Owner must sign the transaction" + invalidPkhData + validCtx + + shouldFailWithErr + "Fail with invalid price data" + "Input does not contain sg" + invalidPriceData + validCtx + +mockSgCs :: CurrencySymbol +mockSgCs = CurrencySymbol "ff" + +collectionNftWithMinAda :: Value +collectionNftWithMinAda = assetClassValue TestValues.collectionNft 1 <> toValue minAdaTxOut + +seabugWithMinAda :: Value +seabugWithMinAda = singleton mockSgCs tn 1 <> toValue minAdaTxOut + where + tn = + mkTokenName $ + NftId (snd . unAssetClass $ TestValues.collectionNft) TestValues.nftPrice TestValues.authorPkh + +validData :: TestData ( 'ForSpending LockDatum LockAct) +validData = SpendingTest dtm redeemer val + where + dtm = LockDatum mockSgCs 1 (snd . unAssetClass $ TestValues.collectionNft) + redeemer = Restake TestValues.authorPkh TestValues.nftPrice + val = collectionNftWithMinAda + +extendedData :: TestData ( 'ForSpending LockDatum LockAct) +extendedData = SpendingTest dtm redeemer val + where + dtm = LockDatum mockSgCs TestValues.testLockupEnd (snd . unAssetClass $ TestValues.collectionNft) + redeemer = Restake TestValues.authorPkh TestValues.nftPrice + val = collectionNftWithMinAda + +invalidPkhData :: TestData ( 'ForSpending LockDatum LockAct) +invalidPkhData = SpendingTest dtm redeemer val + where + dtm = LockDatum mockSgCs 1 (snd . unAssetClass $ TestValues.collectionNft) + redeemer = Restake TestValues.userOnePkh TestValues.nftPrice + val = collectionNftWithMinAda + +invalidPriceData :: TestData ( 'ForSpending LockDatum LockAct) +invalidPriceData = SpendingTest dtm redeemer val + where + dtm = LockDatum mockSgCs 1 (snd . unAssetClass $ TestValues.collectionNft) + redeemer = Restake TestValues.authorPkh TestValues.newNftPrice + val = collectionNftWithMinAda + +validCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +validCtx = + noOwnerSignatureCtx + <> signedWith (unPaymentPubKeyHash TestValues.authorPkh) + +spendCOCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +spendCOCtx = + paysToSelf + (toValue minAdaTxOut) + (LockDatum mockSgCs TestValues.testLockupEnd (snd . unAssetClass $ TestValues.collectionNft)) + <> signedWith (unPaymentPubKeyHash TestValues.authorPkh) + <> paysToPubKey (unPaymentPubKeyHash TestValues.authorPkh) (seabugWithMinAda <> assetClassValue TestValues.collectionNft 1) + +mintSgCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +mintSgCtx = + validCtx + <> mintsValue (singleton mockSgCs "foo" 1) + +noOwnerSignatureCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +noOwnerSignatureCtx = + paysToSelf + collectionNftWithMinAda + (LockDatum mockSgCs TestValues.testLockupEnd (snd . unAssetClass $ TestValues.collectionNft)) + <> paysToPubKey (unPaymentPubKeyHash TestValues.authorPkh) seabugWithMinAda + +noSgCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +noSgCtx = + paysToSelf + collectionNftWithMinAda + (LockDatum mockSgCs TestValues.testLockupEnd (snd . unAssetClass $ TestValues.collectionNft)) + <> signedWith (unPaymentPubKeyHash TestValues.authorPkh) + +extendedCtx :: ContextBuilder ( 'ForSpending LockDatum r) +extendedCtx = + paysToSelf + collectionNftWithMinAda + (LockDatum mockSgCs (TestValues.testLockupEnd + Slot TestValues.testLockup) (snd . unAssetClass $ TestValues.collectionNft)) + <> paysToPubKey (unPaymentPubKeyHash TestValues.authorPkh) seabugWithMinAda + <> signedWith (unPaymentPubKeyHash TestValues.authorPkh) + +mintOtherCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +mintOtherCtx = + validCtx + <> mintsValue (singleton "aabbcc" "Other token" 42) diff --git a/mlabs/test/Test/EfficientNFT/Script/TokenUnstake.hs b/mlabs/test/Test/EfficientNFT/Script/TokenUnstake.hs new file mode 100644 index 000000000..a8ecb9898 --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/TokenUnstake.hs @@ -0,0 +1,155 @@ +module Test.EfficientNFT.Script.TokenUnstake (test) where + +import Prelude ((<>)) + +import Ledger ( + CurrencySymbol, + minAdaTxOut, + ) +import Ledger.Value (CurrencySymbol (CurrencySymbol), Value, assetClassValue, singleton, unAssetClass) +import Plutus.V1.Ledger.Ada (toValue) +import PlutusTx.Prelude hiding (elem, mempty, (<>)) +import Test.Tasty (TestTree, localOption, testGroup) +import Test.Tasty.Plutus.Context (ContextBuilder, Purpose (ForSpending), mintsValue, paysToSelf, paysToWallet) +import Test.Tasty.Plutus.Script.Unit (shouldValidate) +import Test.Tasty.Plutus.TestData (TestData (SpendingTest)) +import Test.Tasty.Plutus.WithScript (withTestScript) + +import Mlabs.EfficientNFT.Types +import Test.EfficientNFT.Script.Values (shouldFailWithErr) +import Test.EfficientNFT.Script.Values qualified as TestValues + +test :: TestTree +test = + testGroup + "Unstaking" + [ localOption TestValues.afterDeadline $ + withTestScript "Unstake CNFT after lockupEnd" TestValues.testLockScript $ do + shouldValidate "Pass with valid data and context" validData validCtx + + shouldFailWithErr + "Fail when lockup is extended" + "Current slot smaller than lockup+entered" + extendedData + validCtx + + sharedAlwaysInvalid + , localOption TestValues.afterDeadlineAndLockup $ + withTestScript "Unstake CNFT after lockupEnd + lockup" TestValues.testLockScript $ do + shouldValidate "Pass with valid data and context" validData validCtx + + shouldValidate + "Pass when lockup is extended" + extendedData + validCtx + + sharedAlwaysInvalid + , localOption TestValues.beforeDeadline $ + withTestScript "Unstake CNFT before lockupEnd" TestValues.testLockScript $ do + shouldFailWithErr + "Fail with valid data and context" + "Current slot smaller than lockupEnd" + validData + validCtx + + sharedAlwaysInvalid + ] + where + sharedAlwaysInvalid = do + shouldFailWithErr + "Fail when not burning sg" + "sgNFT must be burned" + validData + noBurnCtx + + shouldFailWithErr + "Fail when CO exsits" + "CO must not exists" + validData + withCoCtx + + shouldFailWithErr + "Fail with invalid pkh data" + "sgNFT must be burned" + invalidPkhData + validCtx + + shouldFailWithErr + "Fail with invalid price data" + "sgNFT must be burned" + invalidPriceData + validCtx + + shouldFailWithErr + "Fail with invalid Sg CS" + "sgNFT must be burned" + validData + invalidSgCsCtx + + shouldFailWithErr + "Fail with invalid Sg TN" + "sgNFT must be burned" + validData + invalidSgTnCtx + +mockSgCs :: CurrencySymbol +mockSgCs = CurrencySymbol "ff" + +collectionNftWithMinAda :: Value +collectionNftWithMinAda = assetClassValue TestValues.collectionNft 1 <> toValue minAdaTxOut + +validData :: TestData ( 'ForSpending LockDatum LockAct) +validData = SpendingTest dtm redeemer val + where + dtm = LockDatum mockSgCs 1 (snd . unAssetClass $ TestValues.collectionNft) + redeemer = Unstake TestValues.authorPkh TestValues.nftPrice + val = collectionNftWithMinAda + +extendedData :: TestData ( 'ForSpending LockDatum LockAct) +extendedData = SpendingTest dtm redeemer val + where + dtm = LockDatum mockSgCs TestValues.testLockupEnd (snd . unAssetClass $ TestValues.collectionNft) + redeemer = Unstake TestValues.authorPkh TestValues.nftPrice + val = collectionNftWithMinAda + +invalidPkhData :: TestData ( 'ForSpending LockDatum LockAct) +invalidPkhData = SpendingTest dtm redeemer val + where + dtm = LockDatum mockSgCs 1 (snd . unAssetClass $ TestValues.collectionNft) + redeemer = Unstake TestValues.userOnePkh TestValues.nftPrice + val = collectionNftWithMinAda + +invalidPriceData :: TestData ( 'ForSpending LockDatum LockAct) +invalidPriceData = SpendingTest dtm redeemer val + where + dtm = LockDatum mockSgCs 1 (snd . unAssetClass $ TestValues.collectionNft) + redeemer = Unstake TestValues.authorPkh TestValues.newNftPrice + val = collectionNftWithMinAda + +validCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +validCtx = + noBurnCtx + <> mintsValue (singleton mockSgCs TestValues.tokenName (negate 1)) + +noBurnCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +noBurnCtx = + paysToWallet + TestValues.authorWallet + collectionNftWithMinAda + +withCoCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +withCoCtx = + validCtx + <> paysToSelf + (toValue minAdaTxOut) + (LockDatum mockSgCs 1 (snd . unAssetClass $ TestValues.collectionNft)) + +invalidSgCsCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +invalidSgCsCtx = + noBurnCtx + <> mintsValue (singleton "aa" TestValues.tokenName (negate 1)) + +invalidSgTnCtx :: ContextBuilder ( 'ForSpending LockDatum LockAct) +invalidSgTnCtx = + noBurnCtx + <> mintsValue (singleton mockSgCs "I AM INVALID" (negate 1)) diff --git a/mlabs/test/Test/EfficientNFT/Script/Values.hs b/mlabs/test/Test/EfficientNFT/Script/Values.hs new file mode 100644 index 000000000..e1a7e12aa --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Script/Values.hs @@ -0,0 +1,264 @@ +module Test.EfficientNFT.Script.Values ( + authorWallet, + authorPkh, + nftPrice, + tokenName, + daoValHash, + daoShare, + daoShareVal, + authorShare, + authorShareVal, + ownerShareVal, + userOnePkh, + userTwoPkh, + collectionNft, + collection, + nft1, + nft2, + nft3, + burnHash, + mintTxOutRef, + newNftPrice, + newPriceNft1, + otherPkh, + newPriceTokenName, + testTokenPolicy, + testLockup, + testLockupEnd, + testLockScript, + shouldFailWithErr, + afterDeadline, + afterDeadlineAndLockup, + beforeDeadline, + testMarketplaceScript, + testDaoScript, +) where + +import PlutusTx qualified +import PlutusTx.Prelude hiding (elem) + +import Data.Aeson (FromJSON, decode) +import Data.ByteString.Lazy (ByteString) +import Data.Data (Typeable) +import Data.Default (def) +import Data.Maybe (fromJust) +import Data.String (String) +import Ledger ( + AssetClass, + Extended (Finite, PosInf), + Interval (Interval), + LowerBound (LowerBound), + PaymentPubKeyHash (PaymentPubKeyHash), + Slot (Slot), + TokenName, + TxOutRef (TxOutRef), + UpperBound (UpperBound), + ValidatorHash, + unPaymentPubKeyHash, + ) +import Ledger.Ada qualified as Ada +import Ledger.CardanoWallet qualified as CardanoWallet +import Ledger.TimeSlot (slotToBeginPOSIXTime) +import Ledger.Typed.Scripts (validatorHash) +import Ledger.Value (Value) +import Mlabs.EfficientNFT.Token (mkPolicy, mkTokenName) +import Plutus.V1.Ledger.Value (AssetClass (unAssetClass), assetClass) +import PlutusTx.Natural (Natural) +import Test.Tasty.Plutus.Context ( + ContextBuilder, + Purpose (ForMinting, ForSpending), + ) +import Test.Tasty.Plutus.Options (TimeRange (TimeRange)) +import Test.Tasty.Plutus.Script.Unit (shouldn'tValidateTracing) +import Test.Tasty.Plutus.TestData (TestData) +import Test.Tasty.Plutus.TestScript (TestScript, mkTestMintingPolicy, mkTestValidator, toTestMintingPolicy, toTestValidator) +import Test.Tasty.Plutus.WithScript (WithScript) +import Wallet.Emulator.Types qualified as Emu +import Prelude (elem) + +import Mlabs.EfficientNFT.Dao (daoValidator, mkValidator) +import Mlabs.EfficientNFT.Lock (lockValidator, mkValidator) +import Mlabs.EfficientNFT.Marketplace (mkValidator) +import Mlabs.EfficientNFT.Types (LockAct, LockDatum, MarketplaceDatum, MintAct, NftCollection (..), NftId (..)) + +mintTxOutRef :: TxOutRef +mintTxOutRef = TxOutRef txId 1 + where + txId = + unsafeDecode + "{\"getTxId\" : \"3a9e96cbb9e2399046e7b653e29e2cc27ac88b3810b15f448b91425a9a27ef3a\"}" + +authorWallet :: Emu.Wallet +authorWallet = Emu.fromWalletNumber (CardanoWallet.WalletNumber 1) + +authorPkh :: PaymentPubKeyHash +authorPkh = Emu.mockWalletPaymentPubKeyHash authorWallet + +-- User 1 +userOneWallet :: Emu.Wallet +userOneWallet = Emu.fromWalletNumber (CardanoWallet.WalletNumber 2) + +userOnePkh :: Ledger.PaymentPubKeyHash +userOnePkh = Emu.mockWalletPaymentPubKeyHash userOneWallet + +-- User 2 +userTwoWallet :: Emu.Wallet +userTwoWallet = Emu.fromWalletNumber (CardanoWallet.WalletNumber 3) + +userTwoPkh :: Ledger.PaymentPubKeyHash +userTwoPkh = Emu.mockWalletPaymentPubKeyHash userTwoWallet + +nftPrice :: Natural +nftPrice = toEnum 100_000_000 + +daoValHash :: ValidatorHash +daoValHash = validatorHash $ daoValidator [] + +daoShare :: Natural +daoShare = toEnum 10_00 + +daoShareVal :: Value +daoShareVal = Ada.lovelaceValueOf 10_000_000 + +authorShare :: Natural +authorShare = toEnum 15_00 + +authorShareVal :: Value +authorShareVal = Ada.lovelaceValueOf 15_000_000 + +ownerShareVal :: Value +ownerShareVal = Ada.lovelaceValueOf 75_000_000 + +otherPkh :: PaymentPubKeyHash +otherPkh = + PaymentPubKeyHash $ + unsafeDecode + "{\"getPubKeyHash\" : \"75bd24abfdaf5c68d898484d757f715c7b4413ad91a80d3cb0b3660d\"}" + +tokenName :: TokenName +tokenName = mkTokenName nft1 + +newPriceTokenName :: TokenName +newPriceTokenName = mkTokenName newPriceNft1 + +-- newPrice :: Natural +-- newPrice = nftPrice + nftPrice + +-- newPriceTokenName :: TokenName +-- newPriceTokenName = mkTokenName authorPkh newPrice + +unsafeDecode :: FromJSON a => ByteString -> a +unsafeDecode = fromJust . decode + +collectionNft :: AssetClass +collectionNft = assetClass "abcd" "NFT" + +collection :: NftCollection +collection = + NftCollection + { nftCollection'collectionNftCs = fst . unAssetClass $ collectionNft + , nftCollection'lockLockup = 7776000 + , nftCollection'lockLockupEnd = 7776000 + , nftCollection'lockingScript = validatorHash $ lockValidator (fst $ unAssetClass collectionNft) 7776000 7776000 + , nftCollection'author = authorPkh + , nftCollection'authorShare = authorShare + , nftCollection'daoScript = validatorHash $ daoValidator [] + , nftCollection'daoShare = daoShare + } + +nft1 :: NftId +nft1 = + NftId + { nftId'price = nftPrice + , nftId'owner = authorPkh + , nftId'collectionNftTn = snd . unAssetClass $ collectionNft + } + +nft2 :: NftId +nft2 = + nft1 {nftId'owner = userOnePkh} + +nft3 :: NftId +nft3 = + nft1 {nftId'owner = userTwoPkh} + +newNftPrice :: Natural +newNftPrice = nftPrice * toEnum 2 + +newPriceNft1 :: NftId +newPriceNft1 = nft1 {nftId'price = newNftPrice} + +burnHash :: ValidatorHash +burnHash = validatorHash $ lockValidator (fst $ unAssetClass collectionNft) 7776000 7776000 + +testTokenPolicy :: TestScript ( 'ForMinting MintAct) +testTokenPolicy = + mkTestMintingPolicy + ( $$(PlutusTx.compile [||mkPolicy||]) + `PlutusTx.applyCode` PlutusTx.liftCode (nftCollection'collectionNftCs collection) + `PlutusTx.applyCode` PlutusTx.liftCode (nftCollection'lockingScript collection) + `PlutusTx.applyCode` PlutusTx.liftCode (nftCollection'author collection) + `PlutusTx.applyCode` PlutusTx.liftCode (nftCollection'authorShare collection) + `PlutusTx.applyCode` PlutusTx.liftCode (nftCollection'daoScript collection) + `PlutusTx.applyCode` PlutusTx.liftCode (nftCollection'daoShare collection) + ) + $$(PlutusTx.compile [||toTestMintingPolicy||]) + +testLockup :: Integer +testLockup = 7776000 + +testLockupEnd :: Slot +testLockupEnd = 7776000 + +testLockScript :: TestScript ( 'ForSpending LockDatum LockAct) +testLockScript = + mkTestValidator + ( $$(PlutusTx.compile [||Mlabs.EfficientNFT.Lock.mkValidator||]) + `PlutusTx.applyCode` PlutusTx.liftCode (fst . unAssetClass $ collectionNft) + `PlutusTx.applyCode` PlutusTx.liftCode testLockup + `PlutusTx.applyCode` PlutusTx.liftCode testLockupEnd + ) + $$(PlutusTx.compile [||toTestValidator||]) + +shouldFailWithErr :: + forall (p :: Purpose). + Typeable p => + String -> + BuiltinString -> + TestData p -> + ContextBuilder p -> + WithScript p () +shouldFailWithErr name errMsg = + shouldn'tValidateTracing name (errMsg' `elem`) + where + errMsg' = fromBuiltin errMsg + +mkRange :: Slot -> TimeRange +mkRange slot = + TimeRange $ + Interval + (LowerBound (Finite (slotToBeginPOSIXTime def slot)) True) + (UpperBound PosInf False) + +afterDeadline :: TimeRange +afterDeadline = mkRange (testLockupEnd + 1) + +afterDeadlineAndLockup :: TimeRange +afterDeadlineAndLockup = mkRange (testLockupEnd + Slot testLockup + 1) + +beforeDeadline :: TimeRange +beforeDeadline = mkRange (testLockupEnd - 1) + +testMarketplaceScript :: TestScript ( 'ForSpending MarketplaceDatum BuiltinData) +testMarketplaceScript = + mkTestValidator + $$(PlutusTx.compile [||Mlabs.EfficientNFT.Marketplace.mkValidator||]) + $$(PlutusTx.compile [||toTestValidator||]) + +testDaoScript :: TestScript ( 'ForSpending BuiltinData BuiltinData) +testDaoScript = + mkTestValidator + ( $$(PlutusTx.compile [||Mlabs.EfficientNFT.Dao.mkValidator||]) + `PlutusTx.applyCode` PlutusTx.liftCode [unPaymentPubKeyHash userOnePkh, unPaymentPubKeyHash userTwoPkh] + ) + $$(PlutusTx.compile [||toTestValidator||]) diff --git a/mlabs/test/Test/EfficientNFT/Size.hs b/mlabs/test/Test/EfficientNFT/Size.hs new file mode 100644 index 000000000..23f63b782 --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Size.hs @@ -0,0 +1,42 @@ +module Test.EfficientNFT.Size (test) where + +import Plutus.V1.Ledger.Scripts (fromCompiledCode) +import PlutusTx qualified +import PlutusTx.Prelude +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.Plutus.Script.Size (fitsOnChain) + +import Mlabs.EfficientNFT.Dao qualified as Dao +import Mlabs.EfficientNFT.Lock qualified as Lock +import Mlabs.EfficientNFT.Marketplace qualified as Marketplace +import Mlabs.EfficientNFT.Token (mkPolicy) + +test :: TestTree +test = + testGroup + "Size" + [ testMintingPolicyFitOnChain + , testLockScriptFitOnChain + , testMarketplaceScriptFitOnChain + , testDaoScriptFitOnChain + ] + +testMintingPolicyFitOnChain :: TestTree +testMintingPolicyFitOnChain = + fitsOnChain "Minting policy" $ + fromCompiledCode $$(PlutusTx.compile [||mkPolicy||]) + +testLockScriptFitOnChain :: TestTree +testLockScriptFitOnChain = + fitsOnChain "Lock script" $ + fromCompiledCode $$(PlutusTx.compile [||Lock.mkValidator||]) + +testMarketplaceScriptFitOnChain :: TestTree +testMarketplaceScriptFitOnChain = + fitsOnChain "Marketplace script" $ + fromCompiledCode $$(PlutusTx.compile [||Marketplace.mkValidator||]) + +testDaoScriptFitOnChain :: TestTree +testDaoScriptFitOnChain = + fitsOnChain "Dao script" $ + fromCompiledCode $$(PlutusTx.compile [||Dao.mkValidator||]) diff --git a/mlabs/test/Test/EfficientNFT/Trace.hs b/mlabs/test/Test/EfficientNFT/Trace.hs new file mode 100644 index 000000000..708a64d8b --- /dev/null +++ b/mlabs/test/Test/EfficientNFT/Trace.hs @@ -0,0 +1,66 @@ +module Test.EfficientNFT.Trace where + +import PlutusTx.Prelude +import Prelude qualified as Hask + +import Control.Monad (void) +import Control.Monad.Freer.Extras.Log as Extra (logInfo) +import Data.Maybe (fromJust) +import Data.Monoid (Last (..)) +import Plutus.Trace.Emulator (EmulatorTrace, activateContractWallet, callEndpoint, runEmulatorTraceIO) +import Plutus.Trace.Emulator qualified as Trace +import Wallet.Emulator (Wallet) +import Wallet.Emulator qualified as Emulator + +import Mlabs.EfficientNFT.Api +import Mlabs.EfficientNFT.Types +import Mlabs.Utils.Wallet (walletFromNumber) + +type AppTraceHandle a = Trace.ContractHandle NftId NFTAppSchema a + +mintTrace :: Emulator.Wallet -> EmulatorTrace () +mintTrace wallet = do + h1 <- activateContractWallet wallet endpoints + + callEndpoint @"mint" h1 artwork + void $ Trace.waitNSlots 5 + nft1 <- fromJust . getLast Hask.<$> Trace.observableState h1 + logInfo $ Hask.show nft1 + + callEndpoint @"set-price" h1 $ SetPriceParams nft1 (toEnum 7_000_000) + void $ Trace.waitNSlots 5 + nft2 <- fromJust . getLast Hask.<$> Trace.observableState h1 + logInfo $ Hask.show nft2 + + callEndpoint @"marketplace-deposit" h1 nft2 + void $ Trace.waitNSlots 5 + + -- callEndpoint @"marketplace-redeem" h1 nft2 + -- void $ Trace.waitNSlots 5 + + callEndpoint @"marketplace-set-price" h1 $ SetPriceParams nft2 (toEnum 9_000_000) + void $ Trace.waitNSlots 5 + nft3 <- fromJust . getLast Hask.<$> Trace.observableState h1 + logInfo $ Hask.show nft3 + where + -- callEndpoint @"marketplace-redeem" h1 nft3 + -- void $ Trace.waitNSlots 5 + + artwork = + MintParams + { mp'authorShare = toEnum 10 + , mp'daoShare = toEnum 10 + , mp'price = toEnum 5_000_000 + , mp'lockLockup = 5 + , mp'lockLockupEnd = 5 + , mp'fakeAuthor = Nothing + , mp'feeVaultKeys = [] + } + +w1, w2, w3 :: Wallet +w1 = walletFromNumber 1 +w2 = walletFromNumber 2 +w3 = walletFromNumber 3 + +test :: Hask.IO () +test = runEmulatorTraceIO $ mintTrace w1 diff --git a/mlabs/test/Test/Governance/Contract.hs b/mlabs/test/Test/Governance/Contract.hs new file mode 100644 index 000000000..0ac017532 --- /dev/null +++ b/mlabs/test/Test/Governance/Contract.hs @@ -0,0 +1,263 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeApplications #-} + +module Test.Governance.Contract ( + test, +) where + +import Data.Functor (void) +import Data.Text (Text) +import PlutusTx.Prelude hiding (error) +import Prelude (error) + +-- import Data.Monoid ((<>), mempty) + +import Plutus.Contract.Test as PT ( + Wallet, + assertContractError, + assertFailedTransaction, + assertNoFailedTransactions, + checkPredicateOptions, + valueAtAddress, + walletFundsChange, + (.&&.), + ) + +import Mlabs.Plutus.Contract (callEndpoint') +import Plutus.Trace.Emulator (ContractInstanceTag) +import Plutus.Trace.Emulator qualified as Trace +import Plutus.Trace.Emulator.Types (ContractHandle) + +import Control.Monad.Freer (Eff, Members) +import Data.Semigroup (Last) +import Data.Text as T (isInfixOf) +import Test.Tasty (TestTree, testGroup) + +import Mlabs.Governance.Contract.Api ( + Deposit (..), + GovernanceSchema, + Withdraw (..), + ) +import Mlabs.Governance.Contract.Server qualified as Gov +import Test.Governance.Init as Test +import Test.Utils (concatPredicates, next) + +import Ledger.Index (ValidationError (..)) + +import Plutus.Trace.Effects.RunContract (RunContract, StartContract) + +--import Control.Monad.Writer (Monoid(mempty)) + +theContract :: Gov.GovernanceContract () +theContract = Gov.governanceEndpoints Test.acGOV + +type Handle = ContractHandle (Maybe (Last Integer)) GovernanceSchema Text +setup :: + (Members [RunContract, StartContract] effs) => + Wallet -> + (Wallet, Gov.GovernanceContract (), ContractInstanceTag, Eff effs Handle) +setup wallet = (wallet, theContract, Trace.walletInstanceTag wallet, Trace.activateContractWallet wallet theContract) + +test :: TestTree +test = + testGroup + "Contract" + [ testGroup + "Deposit" + [ testDepositHappyPath + , testInsuficcientGOVFails + , testCantDepositWithoutGov + , testCantDepositNegativeAmount1 + , testCantDepositNegativeAmount2 + ] + , testGroup + "Withdraw" + [] + ] + +-- testFullWithdraw +-- , testPartialWithdraw +-- , testCantWithdrawNegativeAmount + +-- deposit tests +testDepositHappyPath :: TestTree +testDepositHappyPath = + let (wallet, _, _, activateWallet) = setup Test.fstWalletWithGOV + depoAmt1 = 10 + depoAmt2 = 40 + depoAmt = depoAmt1 + depoAmt2 + in checkPredicateOptions + Test.checkOptions + "Deposit" + ( assertNoFailedTransactions + .&&. walletFundsChange + wallet + ( Test.gov (negate depoAmt) + <> Test.xgov wallet depoAmt + ) + .&&. valueAtAddress Test.scriptAddress (== Test.gov depoAmt) + ) + $ do + hdl <- activateWallet + void $ callEndpoint' @Deposit hdl (Deposit depoAmt1) + next + void $ callEndpoint' @Deposit hdl (Deposit depoAmt2) + next + +testInsuficcientGOVFails :: TestTree +testInsuficcientGOVFails = + let (wallet, contract, tag, activateWallet) = setup Test.fstWalletWithGOV + errCheck = ("InsufficientFunds" `T.isInfixOf`) -- todo probably matching some concrete error type will be better + in checkPredicateOptions + Test.checkOptions + "Cant deposit more GOV than wallet owns" + ( assertNoFailedTransactions + .&&. assertContractError contract tag errCheck "Should fail with `InsufficientFunds`" + .&&. walletFundsChange wallet mempty + ) + $ do + hdl <- activateWallet + void $ callEndpoint' @Deposit hdl (Deposit 1000) -- TODO get value from wallet + next + +testCantDepositWithoutGov :: TestTree +testCantDepositWithoutGov = + let (wallet, contract, tag, activateWallet) = setup Test.walletNoGOV + errCheck = ("InsufficientFunds" `T.isInfixOf`) + in checkPredicateOptions + Test.checkOptions + "Cant deposit with no GOV in wallet" + ( assertNoFailedTransactions + .&&. assertContractError contract tag errCheck "Should fail with `InsufficientFunds`" + .&&. walletFundsChange wallet mempty + ) + $ do + hdl <- activateWallet + void $ callEndpoint' @Deposit hdl (Deposit 50) + next + +{- A bit special case at the moment: + if we try to deposit negative amount without making (positive) deposit beforehand, + transaction will have to burn xGOV tokens: + (see in `deposit`: `xGovValue = Validation.xgovSingleton params.nft (coerce ownPkh) amnt`) + But without prior deposit wallet won't have xGOV tokens to burn, + so `Contract` will throw `InsufficientFunds` exception +-} +testCantDepositNegativeAmount1 :: TestTree +testCantDepositNegativeAmount1 = + let (wallet, contract, tag, activateWallet) = setup Test.fstWalletWithGOV + errCheck = ("InsufficientFunds" `T.isInfixOf`) + in checkPredicateOptions + Test.checkOptions + "Cant deposit negative GOV case 1" + ( assertNoFailedTransactions + .&&. assertContractError contract tag errCheck "Should fail with `InsufficientFunds`" + .&&. walletFundsChange wallet mempty + ) + $ do + hdl <- activateWallet + void $ callEndpoint' @Deposit hdl (Deposit (negate 2)) + next + +testCantDepositNegativeAmount2 :: TestTree +testCantDepositNegativeAmount2 = checkPredicateOptions Test.checkOptions msg predicates actions + where + msg = "Cannot deposit negative GOV case 2" + + actions = do + hdl <- activateWallet + void $ callEndpoint' @Deposit hdl (Deposit depoAmt) + next + void $ callEndpoint' @Deposit hdl (Deposit (negate 2)) + next + + (wallet, _, _, activateWallet) = setup Test.fstWalletWithGOV + + depoAmt = 20 + + predicates = + concatPredicates + [ assertFailedTransaction errCheck + , walletFundsChange wallet $ mconcat [Test.gov (negate depoAmt), Test.xgov wallet depoAmt] + , valueAtAddress Test.scriptAddress (== Test.gov depoAmt) + ] + where + errCheck _ e _ = case e of + NegativeValue _ -> True + _ -> False + +-- withdraw tests +testFullWithdraw :: TestTree +testFullWithdraw = checkPredicateOptions Test.checkOptions msg predicates actions + where + msg = "Full withdraw" + depoAmt = 50 + (wallet, _, _, activateWallet) = setup Test.fstWalletWithGOV + predicates = + concatPredicates + [ assertNoFailedTransactions + , walletFundsChange wallet mempty + ] + actions = do + hdl <- activateWallet + next + void $ callEndpoint' @Deposit hdl (Deposit depoAmt) + next + void $ callEndpoint' @Withdraw hdl (Withdraw $ Test.xgovEP wallet depoAmt) + next + +testPartialWithdraw :: TestTree +testPartialWithdraw = + let (wallet, _, _, activateWallet) = setup Test.fstWalletWithGOV + depoAmt = 50 + withdrawAmt = 20 + diff = depoAmt - withdrawAmt + in checkPredicateOptions + Test.checkOptions + "Partial withdraw" + ( assertNoFailedTransactions + .&&. walletFundsChange wallet (Test.gov (negate diff) <> Test.xgov wallet diff) + .&&. valueAtAddress Test.scriptAddress (== Test.gov diff) + ) + $ do + hdl <- activateWallet + next + void $ callEndpoint' @Deposit hdl (Deposit depoAmt) + next + void $ callEndpoint' @Withdraw hdl (Withdraw $ Test.xgovEP wallet withdrawAmt) + next + +{- What behaviour expected here: + - failed transaction + - contract error + - withdraw all available + ? +-} +testCantWithdrawMoreThandeposited :: TestTree +testCantWithdrawMoreThandeposited = error "TBD" + +testCantWithdrawNegativeAmount :: TestTree +testCantWithdrawNegativeAmount = + let (wallet, _, _, activateWallet) = setup Test.fstWalletWithGOV + errCheck _ e _ = case e of NegativeValue _ -> True; _ -> False + depoAmt = 50 + in checkPredicateOptions + Test.checkOptions + "Cant withdraw negative xGOV amount" + ( assertFailedTransaction errCheck + .&&. walletFundsChange + wallet + ( Test.gov (negate depoAmt) + <> Test.xgov wallet depoAmt + ) + .&&. valueAtAddress Test.scriptAddress (== Test.gov depoAmt) + ) + $ do + hdl <- activateWallet + void $ callEndpoint' @Deposit hdl (Deposit depoAmt) + next + void $ callEndpoint' @Withdraw hdl (Withdraw $ Test.xgovEP wallet (negate 1)) + next + +testCanWithdrawOnlyxGov :: TestTree +testCanWithdrawOnlyxGov = error "TBD" diff --git a/mlabs/test/Test/Governance/Init.hs b/mlabs/test/Test/Governance/Init.hs new file mode 100644 index 000000000..ef0b5c715 --- /dev/null +++ b/mlabs/test/Test/Governance/Init.hs @@ -0,0 +1,110 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NumericUnderscores #-} + +-- | Init blockchain state for tests +module Test.Governance.Init ( + checkOptions, + fstWalletWithGOV, + sndWalletWithGOV, + walletNoGOV, + adminWallet, + gov, + acGOV, + xgov, + xgovEP, + assertHasErrorOutcome, + scriptAddress, +) where + +import PlutusTx.Prelude +import Prelude () + +import Control.Lens ((&), (.~)) +import Data.Coerce (coerce) +import Data.Map (Map) +import Data.Map qualified as M + +import Mlabs.Governance.Contract.Api qualified as Api +import Mlabs.Governance.Contract.Server qualified as Gov +import Mlabs.Governance.Contract.Validation qualified as Gov + +import Ledger (Address, CurrencySymbol, Value, unPaymentPubKeyHash) +import Ledger qualified +import Mlabs.Utils.Wallet (walletFromNumber) + +import Plutus.Contract.Test ( + CheckOptions, + Outcome (..), + Wallet (..), + assertOutcome, + defaultCheckOptions, + emulatorConfig, + mockWalletPaymentPubKeyHash, + ) + +import Plutus.Trace.Emulator (initialChainState) +import Plutus.V1.Ledger.Ada (adaSymbol, adaToken) +import Plutus.V1.Ledger.Value qualified as Value (singleton) + +import Test.Utils (next) + +acGOV :: Gov.AssetClassGov +acGOV = Gov.AssetClassGov "ff" "GOVToken" + +checkOptions :: CheckOptions +checkOptions = defaultCheckOptions & emulatorConfig . initialChainState .~ Left initialDistribution + +-- | Wallets that are used for testing. +fstWalletWithGOV, sndWalletWithGOV, walletNoGOV, adminWallet :: Wallet +fstWalletWithGOV = walletFromNumber 1 +sndWalletWithGOV = walletFromNumber 2 +walletNoGOV = walletFromNumber 3 +adminWallet = walletFromNumber 4 + +scriptAddress :: Address +scriptAddress = Gov.govAddress acGOV + +-- | Make `GOV` `Value` +gov :: Integer -> Value +gov = Gov.govSingleton acGOV + +-- | Make `xGOV` `Value` +xgov :: Wallet -> Integer -> Value +xgov wallet = + Gov.xgovSingleton + acGOV + (mkPkh wallet) + where + (Gov.AssetClassGov cs tn) = acGOV + mkPkh :: Wallet -> Ledger.PubKeyHash + mkPkh = unPaymentPubKeyHash . mockWalletPaymentPubKeyHash + +xgovEP :: Wallet -> Integer -> [(Ledger.PubKeyHash, Integer)] +xgovEP wallet value = [(mkPkh wallet, value)] + where + (Gov.AssetClassGov cs tn) = acGOV + mkPkh :: Wallet -> Ledger.PubKeyHash + mkPkh = unPaymentPubKeyHash . mockWalletPaymentPubKeyHash + +-- | Make `Ada` `Value` +ada :: Integer -> Value +ada = Value.singleton adaSymbol adaToken + +-- | wallets for tests +initialDistribution :: M.Map Wallet Value +initialDistribution = + M.fromList + [ (fstWalletWithGOV, ada 1000_000_000 <> gov 100) + , (sndWalletWithGOV, ada 1000_000_000 <> gov 100) + , (walletNoGOV, ada 1000_000_000) + , (adminWallet, ada 1000_000_000) + ] + +-- | Assert that contract finished excution with arbitrary error +assertHasErrorOutcome contract tag = + assertOutcome contract tag isFailed + where + isFailed e + | (Failed _) <- e = True + | otherwise = False diff --git a/mlabs/test/Test/Lending/Contract.hs b/mlabs/test/Test/Lending/Contract.hs new file mode 100644 index 000000000..a1941ba08 --- /dev/null +++ b/mlabs/test/Test/Lending/Contract.hs @@ -0,0 +1,394 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeApplications #-} + +-- | Tests for lending application contracts. +module Test.Lending.Contract ( + test, +) where + +import Data.Functor (void) +import Data.Semigroup (Last (..)) + +import PlutusTx.Prelude hiding (Eq (..), mconcat, (<>)) +import Prelude (Eq (..), mconcat, (<>)) + +import Plutus.Contract.Test (Wallet, assertAccumState, checkPredicateOptions) +import Plutus.Trace.Emulator qualified as Trace +import Plutus.Trace.Emulator.Types () +import Plutus.V1.Ledger.Value (assetClassValue) +import PlutusTx.Ratio qualified as R +import Test.Lending.Init ( + aAda, + aCoin1, + aCoin2, + aCoin3, + aToken1, + aToken2, + aToken3, + adaCoin, + checkOptions, + coin1, + coin2, + coin3, + lendexId, + toPubKeyHash, + toUserId, + userAct1, + userAct2, + userAct3, + w1, + w2, + w3, + wAdmin, + ) +import Test.Tasty (TestTree, testGroup) +import Test.Utils (next, wait) + +import Mlabs.Emulator.Scene (Scene, appAddress, appOwns, checkScene, owns) +import Mlabs.Lending.Contract qualified as L +import Mlabs.Lending.Contract.Api (StartLendex (..)) +import Mlabs.Lending.Contract.Api qualified as Api +import Mlabs.Lending.Contract.Emulator.Client qualified as L +import Mlabs.Lending.Contract.Server qualified as Server +import Mlabs.Lending.Logic.Types ( + BadBorrow (..), + CoinCfg (..), + CoinRate (..), + InterestRate (..), + PriceAct (..), + QueryRes (QueryResSupportedCurrencies), + StartParams (..), + SupportedCurrency (..), + UserAct (..), + defaultInterestModel, + ) +import Mlabs.Plutus.Contract (callEndpoint') + +test :: TestTree +test = + testGroup + "Contract" + [ testDeposit + , testBorrow + , testBorrowNoCollateral + , testBorrowNotEnoughCollateral + , testWithdraw + , testRepay + , testLiquidationCall + -- , testQueryAllLendexes -- todo: fix - gets stuck in a loop + -- , testQuerrySupportedCurrencies -- todo: fix + -- , testQueryCurrentBalance + -- , testQueryInsolventAccounts -- todo + ] + where + check msg scene = checkPredicateOptions checkOptions msg (checkScene scene) + testDeposit = check "Deposit (can mint aTokens)" depositScene depositScript + testBorrow = check "Borrow" borrowScene borrowScript + testBorrowNoCollateral = check "Borrow without collateral" borrowWithoutCollateralScene borrowWithoutCollateralScript + testBorrowNotEnoughCollateral = check "Borrow with not enough collateral" borrowNotEnoughCollateralScene borrowNotEnoughCollateralScript + testWithdraw = check "Withdraw (can burn aTokens)" withdrawScene withdrawScript + testRepay = check "Repay" repayScene repayScript + testLiquidationCall = + testGroup + "Liquidation" + [ check "Liquidation call aToken" (liquidationCallScene True) (liquidationCallScript True) + , check "Liquidation call real currency" (liquidationCallScene False) (liquidationCallScript False) + ] + testQueryAllLendexes = check "QueryAllLendexes works" queryAllLendexesScene queryAllLendexesScript + +-- testQueryCurrentBalance = check "QeuryCurrentBalance works" queryCurrentBalanceScene queryCurrentBalanceScript +-- testQueryInsolventAccounts = +-------------------------------------------------------------------------------- +-- deposit test + +-- | 3 users deposit 50 coins to lending app. Each of them uses different coin. +depositScript :: Trace.EmulatorTrace () +depositScript = do + L.callStartLendex lendexId wAdmin . StartLendex $ + StartParams + { sp'coins = + fmap + ( \(coin, aCoin) -> + CoinCfg + { coinCfg'coin = coin + , coinCfg'rate = R.fromInteger 1 + , coinCfg'aToken = aCoin + , coinCfg'interestModel = defaultInterestModel + , coinCfg'liquidationBonus = R.reduce 5 100 + } + ) + [(adaCoin, aAda), (coin1, aToken1), (coin2, aToken2), (coin3, aToken3)] + , sp'initValue = assetClassValue adaCoin 1000 + , sp'admins = [toPubKeyHash wAdmin] + , sp'oracles = [toPubKeyHash wAdmin] + } + wait 5 + userAct1 $ DepositAct 50 coin1 + next + userAct2 $ DepositAct 50 coin2 + next + userAct3 $ DepositAct 50 coin3 + next + +depositScene :: Scene +depositScene = + mconcat + [ appAddress (L.lendexAddress lendexId) + , appOwns [(coin1, 50), (coin2, 50), (coin3, 50), (adaCoin, 1000)] + , user w1 coin1 aCoin1 + , user w2 coin2 aCoin2 + , user w3 coin3 aCoin3 + , wAdmin `owns` [(adaCoin, -1000)] + ] + where + user wal coin aCoin = wal `owns` [(coin, -50), (aCoin, 50)] + +-------------------------------------------------------------------------------- +-- borrow test + +{- | 3 users deposit 50 coins to lending app + and first user borrows in coin2 that he does not own prior to script run. +-} +borrowScript :: Trace.EmulatorTrace () +borrowScript = do + depositScript + userAct1 + AddCollateralAct + { add'asset = coin1 + , add'amount = 50 + } + next + userAct1 $ + BorrowAct + { act'asset = coin2 + , act'amount = 30 + , act'rate = StableRate + } + next + +borrowScene :: Scene +borrowScene = depositScene <> borrowChange + where + borrowChange = + mconcat + [ w1 `owns` [(aCoin1, -50), (coin2, 30)] + , appOwns [(aCoin1, 50), (coin2, -30)] + ] + +-------------------------------------------------------------------------------- +-- borrow without collateral test (It should fail to borrow) + +{- | 3 users deposit 50 coins to lending app + and first user borrows in coin2 that he does not own prior to script run. + But it should fail because user does not set his deposit funds as collateral. +-} +borrowWithoutCollateralScript :: Trace.EmulatorTrace () +borrowWithoutCollateralScript = do + depositScript + next + userAct1 $ + BorrowAct + { act'asset = coin2 + , act'amount = 30 + , act'rate = StableRate + } + next + +borrowWithoutCollateralScene :: Scene +borrowWithoutCollateralScene = depositScene + +-------------------------------------------------------------------------------- +-- borrow without not enough collateral test (It should fail to borrow) + +{- | 3 users deposit 50 coins to lending app + and first user wants to borrow too much. + Only allocation of collateral succeeds for the first user but borrow step should fail. +-} +borrowNotEnoughCollateralScript :: Trace.EmulatorTrace () +borrowNotEnoughCollateralScript = do + depositScript + userAct1 + AddCollateralAct + { add'asset = coin1 + , add'amount = 50 + } + next + userAct1 + BorrowAct + { act'asset = coin2 + , act'amount = 60 + , act'rate = StableRate + } + next + +-- | Only allocation of collateral succeeds but borrow step should fail. +borrowNotEnoughCollateralScene :: Scene +borrowNotEnoughCollateralScene = depositScene <> setCollateralChange + where + setCollateralChange = mconcat [w1 `owns` [(aCoin1, -50)], appOwns [(aCoin1, 50)]] + +-------------------------------------------------------------------------------- +-- withdraw test + +{- | User1 deposits 50 out of 100 and gets back 25. + So we check that user has 75 coins and 25 aCoins +-} +withdrawScript :: Trace.EmulatorTrace () +withdrawScript = do + depositScript + userAct1 + WithdrawAct + { act'amount = 25 + , act'asset = coin1 + } + +withdrawScene :: Scene +withdrawScene = depositScene <> withdrawChange + where + withdrawChange = mconcat [w1 `owns` [(aCoin1, -25), (coin1, 25)], appOwns [(coin1, -25)]] + +-------------------------------------------------------------------------------- +-- repay test + +repayScript :: Trace.EmulatorTrace () +repayScript = do + borrowScript + userAct1 $ + RepayAct + { act'asset = coin2 + , act'amount = 20 + , act'rate = StableRate + } + next + +repayScene :: Scene +repayScene = borrowScene <> repayChange + where + repayChange = mconcat [w1 `owns` [(coin2, -20)], appOwns [(coin2, 20)]] + +-------------------------------------------------------------------------------- +-- liquidation call test + +liquidationCallScript :: Bool -> Trace.EmulatorTrace () +liquidationCallScript receiveAToken = do + borrowScript + priceAct wAdmin $ SetAssetPriceAct coin2 (R.fromInteger 2) + next + userAct2 $ + LiquidationCallAct + { act'collateral = coin1 + , act'debt = BadBorrow (toUserId w1) coin2 + , act'debtToCover = 10 + , act'receiveAToken = receiveAToken + } + next + +liquidationCallScene :: Bool -> Scene +liquidationCallScene receiveAToken = borrowScene <> liquidationCallChange + where + liquidationCallChange = + mconcat + [ w2 `owns` [(receiveCoin, 20), (coin2, -10), (adaCoin, 1)] + , appOwns [(adaCoin, -1), (coin2, 10), (receiveCoin, -20)] + ] + + receiveCoin + | receiveAToken = aCoin1 + | otherwise = coin1 + +-------------------------------------------------------------------------------- +-- queryAllLendexes test + +queryAllLendexesScript :: Trace.EmulatorTrace () +queryAllLendexesScript = do + depositScript + void $ L.queryAllLendexes lendexId w1 (L.QueryAllLendexes sp) + where + sp = + StartParams + { sp'coins = + fmap + ( \(coin, aCoin) -> + CoinCfg + { coinCfg'coin = coin + , coinCfg'rate = R.fromInteger 1 + , coinCfg'aToken = aCoin + , coinCfg'interestModel = defaultInterestModel + , coinCfg'liquidationBonus = R.reduce 5 100 + } + ) + [(adaCoin, aAda), (coin1, aToken1), (coin2, aToken2), (coin3, aToken3)] + , sp'initValue = assetClassValue adaCoin 1000 + , sp'admins = [toPubKeyHash wAdmin] + , sp'oracles = [toPubKeyHash wAdmin] + } + +queryAllLendexesScene :: Scene +queryAllLendexesScene = depositScene + +-------------------------------------------------------------------------------- +-- querry get Current Balance test + +-- TODO Write QueryCurrentBalance TEST + +-- queryCurrentBalanceScript :: Trace.EmulatorTrace () +-- queryCurrentBalanceScript = do +-- depositScript +-- void $ L.queryCurrentBalance lendexId w1 (L.QueryCurrentBalance ()) + +{- | Scene is identical as the State is not changed. + queryCurrentBalanceScene :: Scene + queryCurrentBalanceScene = depositScene +-} + +-------------------------------------------------------------------------------- +-- querry supported currencies test +testQuerrySupportedCurrencies :: TestTree +testQuerrySupportedCurrencies = + checkPredicateOptions + checkOptions + "QuerrySupportedCurrencies" + ( assertAccumState + contract + tag + (== expectedQueryResult) + "contract state after QuerrySupportedCurrencies call doesn't match expected" + ) + $ do + initLendex lendexId + next + hdl <- Trace.activateContractWallet w1 contract + void $ callEndpoint' @Api.QuerySupportedCurrencies hdl (Api.QuerySupportedCurrencies ()) + next + where + initLendex lid = L.callStartLendex lid wAdmin . StartLendex $ sp + contract = Server.queryEndpoints lendexId + tag = Trace.walletInstanceTag w1 + coins = [(adaCoin, aAda, R.reduce 1 1), (coin1, aToken1, R.reduce 1 2)] + expectedQueryResult = + Just . Last . QueryResSupportedCurrencies $ + (\(coin, aCoin, rate) -> SupportedCurrency coin aCoin (CoinRate rate 0)) <$> coins + sp = + StartParams + { sp'coins = + fmap + ( \(coin, aCoin, rate) -> + CoinCfg + { coinCfg'coin = coin + , coinCfg'rate = rate + , coinCfg'aToken = aCoin + , coinCfg'interestModel = defaultInterestModel + , coinCfg'liquidationBonus = R.reduce 5 100 + } + ) + coins + , sp'initValue = assetClassValue adaCoin 1000 + , sp'admins = [toPubKeyHash wAdmin] + , sp'oracles = [toPubKeyHash wAdmin] + } + +-------------------------------------------------- +-- names as in script test + +priceAct :: Wallet -> PriceAct -> Trace.EmulatorTrace () +priceAct = L.callPriceAct lendexId diff --git a/mlabs/test/Test/Lending/Init.hs b/mlabs/test/Test/Lending/Init.hs new file mode 100644 index 000000000..b0756f2a2 --- /dev/null +++ b/mlabs/test/Test/Lending/Init.hs @@ -0,0 +1,117 @@ +{-# LANGUAGE NumericUnderscores #-} + +-- | Init blockchain state for tests +module Test.Lending.Init ( + checkOptions, + wAdmin, + w1, + w2, + w3, + userAct1, + userAct2, + userAct3, + adaCoin, + coin1, + coin2, + coin3, + aAda, + aToken1, + aToken2, + aToken3, + aCoin1, + aCoin2, + aCoin3, + initialDistribution, + toUserId, + toPubKeyHash, + lendexId, + fromToken, +) where + +import Prelude + +import Control.Lens ((&), (.~)) +import Data.Map qualified as M +import Ledger (PaymentPubKeyHash (unPaymentPubKeyHash)) +import Plutus.Contract.Test (CheckOptions, Wallet (..), defaultCheckOptions, emulatorConfig, mockWalletPaymentPubKeyHash) +import Plutus.Trace.Emulator (EmulatorTrace, initialChainState) +import Plutus.V1.Ledger.Ada qualified as Ada +import Plutus.V1.Ledger.Crypto (PubKeyHash (..)) +import Plutus.V1.Ledger.Value (TokenName, Value) +import Plutus.V1.Ledger.Value qualified as Value + +import Mlabs.Lending.Contract.Emulator.Client qualified as L +import Mlabs.Lending.Contract.Forge (currencySymbol) +import Mlabs.Lending.Logic.App qualified as L +import Mlabs.Lending.Logic.Types (Coin, LendexId (..), UserAct (..), UserId (..)) +import Mlabs.Utils.Wallet (walletFromNumber) + +checkOptions :: CheckOptions +checkOptions = defaultCheckOptions & emulatorConfig . initialChainState .~ Left initialDistribution + +-- | Wallets that are used for testing. +wAdmin, w1, w2, w3 :: Wallet +w1 = walletFromNumber 1 +w2 = walletFromNumber 2 +w3 = walletFromNumber 3 +wAdmin = walletFromNumber 4 + +toUserId :: Wallet -> UserId +toUserId = UserId . mockWalletPaymentPubKeyHash + +toPubKeyHash :: Wallet -> PubKeyHash +toPubKeyHash = unPaymentPubKeyHash . mockWalletPaymentPubKeyHash + +-- | Identifier for our lendex platform +lendexId :: LendexId +lendexId = LendexId "MLabs lending platform" + +-- | Showrtcuts for user actions +userAct1, userAct2, userAct3 :: UserAct -> EmulatorTrace () +userAct1 = L.callUserAct lendexId w1 +userAct2 = L.callUserAct lendexId w2 +userAct3 = L.callUserAct lendexId w3 + +-- | Coins which are used for testing +adaCoin, coin1, coin2, coin3 :: Coin +coin1 = L.toCoin "Dollar" +coin2 = L.toCoin "Euro" +coin3 = L.toCoin "Lira" + +{- | Corresponding aTokens. We create aTokens in exchange for to the real coins + on our lending app +-} +aToken1, aToken2, aToken3, aAda :: TokenName +aToken1 = Value.tokenName "aDollar" +aToken2 = Value.tokenName "aEuro" +aToken3 = Value.tokenName "aLira" +aAda = Value.tokenName "aAda" + +adaCoin = Value.AssetClass (Ada.adaSymbol, Ada.adaToken) + +-- | Convert aToken to aCoin +fromToken :: TokenName -> Coin +fromToken aToken = Value.AssetClass (currencySymbol lendexId, aToken) + +-- | aCoins that correspond to real coins +aCoin1, aCoin2, aCoin3 :: Coin +aCoin1 = fromToken aToken1 +aCoin2 = fromToken aToken2 +aCoin3 = fromToken aToken3 + +-- | Initial distribution of wallets for testing +initialDistribution :: M.Map Wallet Value +initialDistribution = + M.fromList + [ (wAdmin, val 2000_000_000) + , (w1, val 1000_000_000 <> v1 100) + , (w2, val 1000_000_000 <> v2 100) + , (w3, val 1000_000_000 <> v3 100) + ] + where + val x = Value.singleton Ada.adaSymbol Ada.adaToken x + + coinVal coin = uncurry Value.singleton (Value.unAssetClass coin) + v1 = coinVal coin1 + v2 = coinVal coin2 + v3 = coinVal coin3 diff --git a/mlabs/test/Test/Lending/Logic.hs b/mlabs/test/Test/Lending/Logic.hs new file mode 100644 index 000000000..5df2f3733 --- /dev/null +++ b/mlabs/test/Test/Lending/Logic.hs @@ -0,0 +1,295 @@ +-- | Tests for logic of state transitions for aave prototype +module Test.Lending.Logic ( + test, + testScript, + fromToken, + testAppConfig, + user1, + user2, + user3, + coin1, + coin2, + coin3, +) where + +import PlutusTx.Prelude + +import Data.Map.Strict qualified as M +import Plutus.V1.Ledger.Crypto (PubKeyHash (..)) +import Plutus.V1.Ledger.Value (AssetClass (AssetClass), CurrencySymbol, TokenName, currencySymbol, tokenName) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (Assertion, testCase) + +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash)) +import Mlabs.Emulator.App (checkWallets, noErrors, someErrors) +import Mlabs.Emulator.Blockchain (BchWallet (..)) +import Mlabs.Lending.Logic.App (AppConfig (AppConfig), LendingApp, Script, priceAct, runLendingApp, toCoin, userAct) +import Mlabs.Lending.Logic.Types ( + BadBorrow (BadBorrow), + Coin, + CoinCfg ( + CoinCfg, + coinCfg'aToken, + coinCfg'coin, + coinCfg'interestModel, + coinCfg'liquidationBonus, + coinCfg'rate + ), + InterestRate (StableRate), + PriceAct (SetAssetPriceAct), + UserAct (..), + UserId (..), + adaCoin, + defaultInterestModel, + ) +import PlutusTx.Ratio qualified as R + +-- | Test suite for a logic of lending application +test :: TestTree +test = + testGroup + "Logic" + [ testCase "Deposit" testDeposit + , testCase "Borrow" testBorrow + , testCase "Borrow without collateral" testBorrowNoCollateral + , testCase "Borrow with not enough collateral" testBorrowNotEnoughCollateral + , testCase "Withdraw" testWithdraw + , testCase "Repay" testRepay + , testGroup "Borrow liquidation" testLiquidationCall + , testCase "Wrong user sets the price" testWrongUserPriceSet + ] + where + testBorrow = testWallets [(user1, w1)] borrowScript + where + w1 = BchWallet $ M.fromList [(coin1, 50), (coin2, 30), (aCoin1, 0)] + + testDeposit = testWallets [(user1, wal coin1 aToken1), (user2, wal coin2 aToken2), (user3, wal coin3 aToken3)] depositScript + where + wal coin aToken = BchWallet $ M.fromList [(coin, 50), (fromToken aToken, 50)] + + testBorrowNoCollateral = someErrors $ testScript borrowNoCollateralScript + testBorrowNotEnoughCollateral = someErrors $ testScript borrowNotEnoughCollateralScript + + testWithdraw = testWallets [(user1, w1)] withdrawScript + where + w1 = BchWallet $ M.fromList [(coin1, 75), (aCoin1, 25)] + + -- User: + -- * deposits 50 coin1 + -- * sets it all as collateral + -- * borrows 30 coin2 + -- * repays 20 coin2 back + -- + -- So we get: + -- coin1 - 50 + -- coin2 - 10 = 30 - 20 + -- aToken - 0 = remaining from collateral + testRepay = testWallets [(user1, w1)] repayScript + where + w1 = BchWallet $ M.fromList [(coin1, 50), (coin2, 10), (fromToken aToken1, 0)] + + testLiquidationCall = + [ testCase "get aTokens for collateral" $ + testWallets [(user1, w1), (user2, w2a)] $ liquidationCallScript True + , testCase "get underlying currency for collateral" $ + testWallets [(user1, w1), (user2, w2)] $ liquidationCallScript False + ] + where + w1 = BchWallet $ M.fromList [(coin1, 50), (coin2, 30), (fromToken aToken1, 0)] + -- receive aTokens + w2a = BchWallet $ M.fromList [(coin2, 40), (aCoin2, 50), (aCoin1, 20), (adaCoin, 1)] + -- receive underlying currency + w2 = BchWallet $ M.fromList [(coin2, 40), (aCoin2, 50), (coin1, 20), (adaCoin, 1)] + + testWrongUserPriceSet = someErrors $ testScript wrongUserPriceSetScript + +-- | Checks that script runs without errors +testScript :: Script -> LendingApp +testScript = runLendingApp testAppConfig + +-- | Checks that we have those wallets after script was run. +testWallets :: [(UserId, BchWallet)] -> Script -> Assertion +testWallets wals script = do + noErrors app + checkWallets wals app + where + app = runLendingApp testAppConfig script + +-- | 3 users deposit 50 coins to lending app +depositScript :: Script +depositScript = do + userAct user1 $ DepositAct 50 coin1 + userAct user2 $ DepositAct 50 coin2 + userAct user3 $ DepositAct 50 coin3 + +{- | 3 users deposit 50 coins to lending app + and first user borrows in coin2 that he does not own prior to script run. +-} +borrowScript :: Script +borrowScript = do + depositScript + userAct user1 $ + AddCollateralAct + { add'asset = coin1 + , add'amount = 50 + } + userAct user1 $ + BorrowAct + { act'asset = coin2 + , act'amount = 30 + , act'rate = StableRate + } + +-- | Try to borrow without setting up deposit as collateral. +borrowNoCollateralScript :: Script +borrowNoCollateralScript = do + depositScript + userAct user1 $ + BorrowAct + { act'asset = coin2 + , act'amount = 30 + , act'rate = StableRate + } + +-- | Try to borrow more than collateral permits +borrowNotEnoughCollateralScript :: Script +borrowNotEnoughCollateralScript = do + depositScript + userAct user1 $ + AddCollateralAct + { add'asset = coin1 + , add'amount = 50 + } + userAct user1 $ + BorrowAct + { act'asset = coin2 + , act'amount = 60 + , act'rate = StableRate + } + +{- | User1 deposits 50 out of 100 and gets back 25. + So we check that user has 75 coins and 25 aCoins +-} +withdrawScript :: Script +withdrawScript = do + depositScript + userAct user1 $ + WithdrawAct + { act'amount = 25 + , act'asset = coin1 + } + +{- | We use borrow script to deposit and borrow for user 1 + and then repay part of the borrow. +-} +repayScript :: Script +repayScript = do + borrowScript + userAct user1 $ + RepayAct + { act'asset = coin2 + , act'amount = 20 + , act'rate = StableRate + } + +{- | + * User 1 lends in coin1 and borrows in coin2 + * price for coin2 grows so that collateral is not enough + * health check for user 1 becomes bad + * user 2 repays part of the borrow and aquires part of the collateral of the user 1 + + So we should get the balances + + * init | user1 = 100 $ | user2 = 100 € + * after deposit | user1 = 50 $, 50 a$ | user2 = 50 €, 50 a€ + * after borrow | user1 = 50 $, 30 € | user2 = 50 €, 50 a€ + * after liq call | user1 = 50 $, 30 € | user2 = 40 €, 50 a€, 20 a$, 1 ada : if flag is True + * after liq call | user1 = 50 $, 30 € | user2 = 40 €, 50 a€, 20 $, 1 ada : if flag is False + + user2 pays 10 € for borrow, because at that time Euro to Dollar is 2:1 user2 + gets 20 aDollars, and 1 ada as bonus (5% of the collateral (20) which is rounded). + User gets aDolars because user provides recieveATokens set to True +-} +liquidationCallScript :: Bool -> Script +liquidationCallScript receiveAToken = do + borrowScript + priceAct user1 $ SetAssetPriceAct coin2 (R.fromInteger 2) + userAct user2 $ + LiquidationCallAct + { act'collateral = coin1 + , act'debt = BadBorrow user1 coin2 + , act'debtToCover = 10 + , act'receiveAToken = receiveAToken + } + +-- oracles + +wrongUserPriceSetScript :: Script +wrongUserPriceSetScript = do + priceAct user2 $ SetAssetPriceAct coin2 (R.fromInteger 2) + +--------------------------------- +-- constants + +-- | convert aToken to aCoin +fromToken :: TokenName -> Coin +fromToken aToken = AssetClass (lendingPoolCurrency, aToken) + +-- | Base currency of lending app (it's mock for monetary policy of the lending app) +lendingPoolCurrency :: CurrencySymbol +lendingPoolCurrency = currencySymbol "lending-pool" + +-- users +user1, user2, user3 :: UserId +user1 = UserId $ PaymentPubKeyHash $ PubKeyHash "1" +user2 = UserId $ PaymentPubKeyHash $ PubKeyHash "2" +user3 = UserId $ PaymentPubKeyHash $ PubKeyHash "3" + +-- coins +coin1, coin2, coin3 :: Coin +coin1 = toCoin "Dollar" +coin2 = toCoin "Euro" +coin3 = toCoin "Lira" + +-- | aTokens +aToken1, aToken2, aToken3 :: TokenName +aToken1 = tokenName "aDollar" +aToken2 = tokenName "aEuro" +aToken3 = tokenName "aLira" + +aCoin1, aCoin2 :: Coin +aCoin1 = fromToken aToken1 +aCoin2 = fromToken aToken2 + +-- aCoin3 = fromToken aToken3 + +{- | Default application. + It allocates three users and three reserves for Dollars, Euros and Liras. + Each user has 100 units of only one currency. User 1 has dollars, user 2 has euros and user 3 has liras. +-} +testAppConfig :: AppConfig +testAppConfig = AppConfig reserves users lendingPoolCurrency admins oracles + where + admins = [user1] + oracles = [user1] + + reserves = + fmap + ( \(coin, aCoin) -> + CoinCfg + { coinCfg'coin = coin + , coinCfg'rate = R.fromInteger 1 + , coinCfg'aToken = aCoin + , coinCfg'interestModel = defaultInterestModel + , coinCfg'liquidationBonus = R.reduce 5 100 + } + ) + [(coin1, aToken1), (coin2, aToken2), (coin3, aToken3)] + + users = + [ (Self, wal (adaCoin, 1000)) -- script starts with some ada on it + , (user1, wal (coin1, 100)) + , (user2, wal (coin2, 100)) + , (user3, wal (coin3, 100)) + ] + wal cs = BchWallet $ uncurry M.singleton cs diff --git a/mlabs/test/Test/Lending/QuickCheck.hs b/mlabs/test/Test/Lending/QuickCheck.hs new file mode 100644 index 000000000..d5eb345e1 --- /dev/null +++ b/mlabs/test/Test/Lending/QuickCheck.hs @@ -0,0 +1,151 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE UndecidableInstances #-} + +module Test.Lending.QuickCheck where + +import PlutusTx.Prelude hiding (abs, fmap, length, (<$>), (<*>)) +import Prelude ( + Int, + Show, + abs, + drop, + fmap, + length, + zip3, + (<$>), + (<*>), + ) + +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Plutus.V1.Ledger.Value qualified as Value +import Test.Lending.Logic (coin1, coin2, coin3, fromToken, testAppConfig, user1, user2, user3) +import Test.QuickCheck qualified as QC +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.QuickCheck (testProperty) + +import Mlabs.Emulator.App (App (..), lookupAppWallet) +import Mlabs.Emulator.Blockchain (BchWallet (..)) +import Mlabs.Emulator.Types (Coin, UserId (..), adaCoin) +import Mlabs.Lending.Logic.App (AppConfig (..), Script, runLendingApp, userAct) +import Mlabs.Lending.Logic.Types (UserAct (..)) + +allUsers :: [UserId] +allUsers = [Self, user1, user2, user3] + +users :: [UserId] +users = drop 1 allUsers + +coins :: [Coin] +coins = [adaCoin, coin1, coin2, coin3] + +nonNativeCoins :: [Coin] +nonNativeCoins = drop 1 coins + +aToken :: Coin -> Value.TokenName +aToken (Value.AssetClass (_, Value.TokenName tn)) = Value.TokenName ("a" <> tn) + +aCoin :: Coin -> Coin +aCoin coin = fromToken (aToken coin) + +-- Various integer generators +smallGenSize :: Int +smallGenSize = 100 + +bigGenSize :: Int +bigGenSize = 1_000_000_000_000_000_000 + +positiveSmallInteger :: QC.Gen Integer +positiveSmallInteger = fmap QC.getPositive (QC.resize smallGenSize QC.arbitrary) + +positiveBigInteger :: QC.Gen Integer +positiveBigInteger = (*) <$> gen <*> gen + where + gen = fmap QC.getPositive (QC.resize bigGenSize QC.arbitrary) + +nonPositiveSmallInteger :: QC.Gen Integer +nonPositiveSmallInteger = fmap (negate . abs) (QC.resize smallGenSize QC.arbitrary) + +nonPositiveBigInteger :: QC.Gen Integer +nonPositiveBigInteger = (\x y -> negate (abs (x * y))) <$> gen <*> gen + where + gen = fmap negate (QC.resize bigGenSize QC.arbitrary) + +positiveInteger :: QC.Gen Integer +positiveInteger = QC.frequency [(1, positiveSmallInteger), (1, positiveBigInteger)] + +nonPositiveInteger :: QC.Gen Integer +nonPositiveInteger = QC.frequency [(1, nonPositiveSmallInteger), (1, nonPositiveBigInteger)] + +-- | Contains parameters that deposit test cases can be generalized over +newtype DepositTestInput = DepositTestInput + {deposits :: [(UserId, Coin, Integer)]} + deriving (Show) + +-- | Construct a `Script` +createDepositScript :: DepositTestInput -> Script +createDepositScript (DepositTestInput ds) = + mapM_ (\(user, coin, amt) -> userAct user $ DepositAct amt coin) ds + +noErrorsProp :: App st act -> Bool +noErrorsProp App {..} = null app'log + +someErrorsProp :: App st act -> Bool +someErrorsProp = not . noErrorsProp + +hasWallet :: App st act -> UserId -> BchWallet -> Bool +hasWallet app uid wal = lookupAppWallet uid app == Just wal + +checkWalletsProp :: (Show act, Show st) => [(UserId, BchWallet)] -> App st act -> Bool +checkWalletsProp wals app = all (uncurry $ hasWallet app) wals + +-- Map manipulation helper functions +walletListToNestedMap :: [(UserId, BchWallet)] -> Map UserId (Map Coin Integer) +walletListToNestedMap wals = + addNestedMaps $ map (\(user, BchWallet wal) -> Map.singleton user wal) wals + +nestedMapToWalletList :: Map UserId (Map Coin Integer) -> [(UserId, BchWallet)] +nestedMapToWalletList m = Map.toAscList (Map.map BchWallet m) + +addNestedMaps :: [Map UserId (Map Coin Integer)] -> Map UserId (Map Coin Integer) +addNestedMaps = Map.unionsWith (Map.unionWith (+)) + +-- | Calculate expected balances after running deposit script +expectedWalletsDeposit :: AppConfig -> DepositTestInput -> [(UserId, BchWallet)] +expectedWalletsDeposit appCfg (DepositTestInput ds) = + let startingBalances = walletListToNestedMap (appConfig'users appCfg) + depositedCoins = map (\(user, coin, amt) -> Map.singleton user (Map.singleton coin (negate amt))) ds + aCoins = map (\(user, coin, amt) -> Map.singleton user (Map.singleton (aCoin coin) amt)) ds + appCoins = Map.singleton Self $ Map.unionsWith (+) (map (\(_, coin, amt) -> Map.singleton coin amt) ds) + appAcoins = Map.singleton Self $ Map.fromList $ map (\(_, coin, _) -> (aCoin coin, 0)) ds + allWallets = addNestedMaps ([startingBalances] ++ depositedCoins ++ aCoins ++ [appCoins] ++ [appAcoins]) + in Map.toAscList (Map.map BchWallet allWallets) + +-- | Check that the balances after deposit script run correspond to the expected balances +testWalletsProp :: [(UserId, BchWallet)] -> Script -> Bool +testWalletsProp expectedWals script = + let app = runLendingApp testAppConfig script + in noErrorsProp app && checkWalletsProp expectedWals app + +testWalletsProp' :: DepositTestInput -> Bool +testWalletsProp' d = + let script = createDepositScript d + in testWalletsProp (expectedWalletsDeposit testAppConfig d) script + +depositInputGen :: QC.Gen Integer -> QC.Gen DepositTestInput +depositInputGen integerGen = + fmap (DepositTestInput . zip3 users nonNativeCoins) (QC.vectorOf n integerGen) + where + n = length users + +testDepositLogic :: QC.Property +testDepositLogic = QC.forAll (depositInputGen (QC.choose (1, 100))) testWalletsProp' + +test :: TestTree +test = testGroup "QuickCheck" [testGroup "Logic" [testProperty "deposit" testDepositLogic]] diff --git a/mlabs/test/Test/NFT/Contract.hs b/mlabs/test/Test/NFT/Contract.hs new file mode 100644 index 000000000..91fbd31d7 --- /dev/null +++ b/mlabs/test/Test/NFT/Contract.hs @@ -0,0 +1,339 @@ +module Test.NFT.Contract ( + test, +) where + +import Control.Monad (void) +import Data.Default (def) +import Data.List (sortOn) +import Data.Text qualified as T +import Ledger.Index (ValidationError (..)) +import Ledger.Scripts (ScriptError (..)) +import Ledger.TimeSlot (slotToBeginPOSIXTime) +import Plutus.Contract.Test (assertFailedTransaction) +import Plutus.V1.Ledger.Value (AssetClass (..), flattenValue) +import PlutusTx.Prelude hiding (check, mconcat) +import PlutusTx.Ratio qualified as R +import Test.Tasty (TestTree, testGroup) +import Prelude (mconcat) +import Prelude qualified as Hask + +import Mlabs.Emulator.Scene (checkScene, owns) +import Mlabs.NFT.Contract.Aux (hashData) +import Mlabs.NFT.Contract.Mint (mintParamsToInfo) +import Mlabs.NFT.Contract.Query (queryContentLog, queryCurrentOwnerLog, queryCurrentPriceLog, queryListNftsLog) +import Mlabs.NFT.Spooky (toSpooky) +import Mlabs.NFT.Types ( + AuctionBidParams (..), + AuctionCloseParams (..), + AuctionOpenParams (..), + BuyRequestUser (..), + MintParams (..), + NftId (..), + QueryResponse (..), + SetPriceParams (..), + info'id, + ) +import Test.NFT.Init ( + artwork1, + artwork2, + callStartNftFail, + check, + containsLog, + mkFreeGov, + noChangesScene, + ownsAda, + toUserId, + userBidAuction, + userBuy, + userCloseAuction, + userMint, + userQueryContent, + userQueryListNfts, + userQueryOwner, + userQueryPrice, + userSetPrice, + userStartAuction, + userWait, + w1, + w2, + w3, + wA, + ) + +test :: TestTree +test = + testGroup + "Contract" + [ -- testInitApp + testBuyOnce + , testBuyTwice + , testChangePriceWithoutOwnership + , testBuyLockedScript + , testBuyNotEnoughPriceScript + , -- , testGroup + -- "Auction" + -- [ testAuctionOneBid + -- , testAuctionOneBidNoClosing + -- , testAuctionManyBids + -- , testBidAfterDeadline + -- , testAuctionWithPrice + -- , testSetPriceDuringAuction + -- ] + testGroup + "Query" + [ testQueryPrice + , testQueryOwner + , testQueryListNfts + , testQueryContent + ] + ] + +-- | Test initialisation of an app instance + +{- `check` performs app initialization first, + so this test checks two cases: + - when admin initiates contract + - when not admin initiates contract +-} +testInitApp :: TestTree +testInitApp = check "Init app" assertState wA script + where + script = callStartNftFail wA + assertState = + assertFailedTransaction + ( \_ vEr _ -> + case vEr of + (ScriptFailure (EvaluationError (er : _) _)) -> msg Hask.== T.unpack er + _ -> False + ) + msg = "Only an admin can initialise app." + +-- | User 2 buys from user 1 +testBuyOnce :: TestTree +testBuyOnce = check "Buy once" (checkScene scene) wA script + where + script = do + nft1 <- userMint w1 artwork1 + userSetPrice w1 $ SetPriceParams nft1 (Just 1_000_000) + userBuy w2 $ BuyRequestUser nft1 1_000_000 Nothing + userSetPrice w2 $ SetPriceParams nft1 (Just 2_000_000) + scene = + mconcat + [ w1 `ownsAda` subtractFee 1_000_000 + , w2 `ownsGov` calcFee 1_000_000 + , w2 `ownsAda` (-1_000_000) + , wA `ownsAda` calcFee 1_000_000 + ] + +{- | +- * User 2 buys from user 1 +- * User 3 buys from user 2 +-} +testBuyTwice :: TestTree +testBuyTwice = check "Buy twice" (checkScene scene) wA script + where + script = do + nft1 <- userMint w1 artwork1 + userSetPrice w1 $ SetPriceParams nft1 (Just 1_000_000) + userBuy w2 $ BuyRequestUser nft1 1_000_000 Nothing + userSetPrice w2 $ SetPriceParams nft1 (Just 2_000_000) + userBuy w3 $ BuyRequestUser nft1 2_000_000 Nothing + scene = + mconcat + [ w1 `ownsAda` subtractFee 1_200_000 + , w2 `ownsGov` calcFee 1_000_000 + , w2 `ownsAda` (subtractFee 1_800_000 - 1_000_000) + , w3 `ownsGov` calcFee 2_000_000 + , w3 `ownsAda` (-2_000_000) + , wA `ownsAda` calcFee 3_000_000 + ] + +-- | User 1 tries to set price after user 2 owned the NFT. +testChangePriceWithoutOwnership :: TestTree +testChangePriceWithoutOwnership = check "Sets price without ownership" (checkScene scene) wA script + where + script = do + nft1 <- userMint w1 artwork1 + userSetPrice w1 $ SetPriceParams nft1 (Just 1_000_000) + userBuy w2 $ BuyRequestUser nft1 1_000_000 Nothing + userSetPrice w1 $ SetPriceParams nft1 (Just 2_000_000) + userBuy w3 $ BuyRequestUser nft1 2_000_000 Nothing + scene = + mconcat + [ w1 `ownsAda` subtractFee 1_000_000 + , w2 `ownsGov` calcFee 1_000_000 + , w2 `ownsAda` (-1_000_000) + , wA `ownsAda` calcFee 1_000_000 + ] + +-- | User 2 tries to buy NFT which is locked (no price is set) +testBuyLockedScript :: TestTree +testBuyLockedScript = check "Buy locked NFT" (checkScene noChangesScene) wA script + where + script = do + nft1 <- userMint w1 artwork1 + userBuy w2 $ BuyRequestUser nft1 1_000_000 Nothing + +-- | User 2 tries to buy open NFT with not enough money +testBuyNotEnoughPriceScript :: TestTree +testBuyNotEnoughPriceScript = check "Buy not enough price" (checkScene noChangesScene) wA script + where + script = do + nft1 <- userMint w1 artwork1 + userSetPrice w1 $ SetPriceParams nft1 (Just 1_000_000) + userBuy w2 $ BuyRequestUser nft1 500_000 Nothing + +testAuctionOneBid :: TestTree +testAuctionOneBid = check "Single bid" (checkScene scene) wA script + where + script = do + nft1 <- userMint w1 artwork1 + userStartAuction w1 $ AuctionOpenParams nft1 (slotToBeginPOSIXTime def 20) 0 + userBidAuction w2 $ AuctionBidParams nft1 1_000_000 + userWait 20 + userCloseAuction w1 $ AuctionCloseParams nft1 + userWait 3 + scene = + mconcat + [ w1 `ownsAda` subtractFee 1_000_000 + , w2 `ownsGov` calcFee 1_000_000 + , w2 `ownsAda` (-1_000_000) + , wA `ownsAda` calcFee 1_000_000 + ] + +testAuctionOneBidNoClosing :: TestTree +testAuctionOneBidNoClosing = check "Single bid without closing" (checkScene scene) wA script + where + script = do + nft1 <- userMint w1 artwork1 + userStartAuction w1 $ AuctionOpenParams nft1 (slotToBeginPOSIXTime def 9999) 500_000 + userBidAuction w1 $ AuctionBidParams nft1 1_000_000 + void $ userMint w1 artwork2 + + scene = + mconcat + [ w1 `ownsAda` (-1_000_000) + ] + +testAuctionManyBids :: TestTree +testAuctionManyBids = check "Multiple bids" (checkScene scene) wA script + where + script = do + nft1 <- userMint w1 artwork1 + userStartAuction w1 $ AuctionOpenParams nft1 (slotToBeginPOSIXTime def 20) 0 + userBidAuction w3 $ AuctionBidParams nft1 200_000 + userBidAuction w2 $ AuctionBidParams nft1 1_000_000 + scene = + mconcat + [ w2 `ownsAda` (-1_000_000) + ] + +testAuctionWithPrice :: TestTree +testAuctionWithPrice = check "Starting auction overrides price" (containsLog w1 msg) wA script + where + script = do + nft2 <- userMint w1 artwork2 + userSetPrice w1 $ SetPriceParams nft2 (Just 1_000_000) + userStartAuction w1 $ AuctionOpenParams nft2 (slotToBeginPOSIXTime def 20) 500_000 + userQueryPrice w1 nft2 + + nftId = NftId . toSpooky . hashData . mp'content $ artwork2 + price = QueryCurrentPrice Nothing + msg = queryCurrentPriceLog nftId price + +testSetPriceDuringAuction :: TestTree +testSetPriceDuringAuction = check "Cannot set price during auction" (containsLog w1 msg) wA script + where + script = do + nft2 <- userMint w1 artwork2 + userSetPrice w1 $ SetPriceParams nft2 (Just 1_000_000) + userStartAuction w1 $ AuctionOpenParams nft2 (slotToBeginPOSIXTime def 20) 500_000 + userQueryPrice w1 nft2 + userSetPrice w1 $ SetPriceParams nft2 (Just 1_000_000) + + nftId = NftId . toSpooky . hashData . mp'content $ artwork2 + price = QueryCurrentPrice Nothing + msg = queryCurrentPriceLog nftId price + +testBidAfterDeadline :: TestTree +testBidAfterDeadline = check "Cannot bid after deadline" (checkScene scene) wA script + where + script = do + nft1 <- userMint w1 artwork1 + userStartAuction w1 $ AuctionOpenParams nft1 (slotToBeginPOSIXTime def 20) 0 + -- userBidAuction w3 $ AuctionBidParams nft1 100_000 + userBidAuction w2 $ AuctionBidParams nft1 1_000_000 + userBidAuction w3 $ AuctionBidParams nft1 200_000 + + userWait 20 + userCloseAuction w1 $ AuctionCloseParams nft1 + userWait 3 + userBidAuction w3 $ AuctionBidParams nft1 1_500_000 + + scene = + mconcat + [ w1 `ownsAda` subtractFee 1_000_000 + , w2 `ownsGov` calcFee 1_000_000 + , w2 `ownsAda` (-1_000_000) + , wA `ownsAda` calcFee 1_000_000 + ] + +-- | User checks the price of the artwork. +testQueryPrice :: TestTree +testQueryPrice = check "Query price" (containsLog w1 msg) wA script + where + script = do + nft2 <- userMint w1 artwork2 + userQueryPrice w1 nft2 + + nftId = NftId . toSpooky . hashData . mp'content $ artwork2 + price = QueryCurrentPrice . mp'price $ artwork2 + msg = queryCurrentPriceLog nftId price + +testQueryOwner :: TestTree +testQueryOwner = check "Query owner" (containsLog w1 msg) wA script + where + script = do + nft2 <- userMint w1 artwork2 + userQueryOwner w1 nft2 + + nftId = NftId . toSpooky . hashData . mp'content $ artwork2 + owner = QueryCurrentOwner . Just . toUserId $ w1 + msg = queryCurrentOwnerLog nftId owner + +-- | User lists all NFTs in app +testQueryListNfts :: TestTree +testQueryListNfts = check "Query list NFTs" (containsLog w1 msg) wA script + where + script = do + mapM_ (userMint w1) artworks + userQueryListNfts w1 + + artworks = [artwork1, artwork2] + + nfts = + sortOn info'id + . fmap (\mp -> mintParamsToInfo mp (toUserId w1)) + $ artworks + + msg = queryListNftsLog nfts + +testQueryContent :: TestTree +testQueryContent = check "Query content" (containsLog w1 msg) wA script + where + script = do + nftId <- userMint w1 artwork2 + userQueryContent w1 $ mp'content artwork2 + + content = mp'content artwork2 + msg = queryContentLog content $ QueryContent $ Just infoNft + userId = toUserId w1 + infoNft = mintParamsToInfo artwork2 userId + +subtractFee price = price - calcFee price + +calcFee price = round (fromInteger price * feeRate) + +feeRate = R.reduce 5 1000 + +ownsGov wal am = wal `owns` (fmap (\(cur, tn, amt) -> (AssetClass (cur, tn), amt)) . flattenValue $ mkFreeGov wal am) diff --git a/mlabs/test/Test/NFT/Init.hs b/mlabs/test/Test/NFT/Init.hs new file mode 100644 index 000000000..ac0f16959 --- /dev/null +++ b/mlabs/test/Test/NFT/Init.hs @@ -0,0 +1,352 @@ +module Test.NFT.Init ( + artwork1, + artwork2, + callStartNft, + callStartNftFail, + check, + checkOptions, + noChangesScene, + ownsAda, + runScript, + toUserId, + userBuy, + userMint, + userQueryPrice, + userQueryOwner, + userQueryListNfts, + userQueryContent, + userSetPrice, + containsLog, + w1, + w2, + w3, + wA, + userBidAuction, + userStartAuction, + userCloseAuction, + userWait, + waitInit, + mkFreeGov, + getFreeGov, + appSymbol, +) where + +import Control.Lens ((&), (.~)) +import Control.Monad.Freer (Eff) +import Control.Monad.Freer.Error (Error) +import Control.Monad.Freer.Extras.Log (LogMsg) +import Control.Monad.Reader (ReaderT, ask, lift, runReaderT, void) +import Data.Aeson (Value (String)) +import Data.Map qualified as M +import Data.Monoid (Last (..)) +import Data.Text qualified as T + +-- import Ledger (PaymentPubKeyHash (unPaymentPubKeyHash), getPubKeyHash) +import Numeric.Natural (Natural) +import Plutus.Contract.Test ( + CheckOptions, + TracePredicate, + Wallet (..), + assertInstanceLog, + checkPredicateOptions, + defaultCheckOptions, + emulatorConfig, + mockWalletPaymentPubKeyHash, + ) +import Plutus.Trace.Effects.Assert (Assert) +import Plutus.Trace.Effects.EmulatedWalletAPI (EmulatedWalletAPI) +import Plutus.Trace.Effects.EmulatorControl (EmulatorControl) +import Plutus.Trace.Effects.RunContract (RunContract, StartContract) +import Plutus.Trace.Effects.Waiting (Waiting) +import Plutus.Trace.Emulator ( + EmulatorRuntimeError (GenericError), + EmulatorTrace, + activateContractWallet, + callEndpoint, + initialChainState, + observableState, + throwError, + waitNSlots, + ) +import Plutus.Trace.Emulator.Types (ContractInstanceLog (..), ContractInstanceMsg (..), walletInstanceTag) +import Plutus.V1.Ledger.Ada (adaSymbol, adaToken) +import PlutusTx.Ratio qualified as R + +-- import Plutus.V1.Ledger.Api (getPubKeyHash) +import Plutus.V1.Ledger.Value (AssetClass (..), CurrencySymbol, TokenName (..), Value, assetClassValue, singleton, valueOf) +import PlutusTx.Prelude hiding (check, foldMap, pure) +import Wallet.Emulator.MultiAgent (EmulatorTimeEvent (..)) +import Prelude (Applicative (..), String, foldMap) +import Prelude qualified as Hask + +import Test.Tasty (TestTree) +import Test.Utils (next) + +import Mlabs.Emulator.Scene (Scene, owns) +import Mlabs.Emulator.Types (adaCoin) +import Mlabs.NFT.Api ( + adminEndpoints, + endpoints, + queryEndpoints, + ) +import Mlabs.NFT.Spooky ( + getPubKeyHash, + toSpooky, + toSpookyAssetClass, + toSpookyPaymentPubKeyHash, + toSpookyPubKeyHash, + unPaymentPubKeyHash, + unSpookyPubKeyHash, + ) +import Mlabs.NFT.Types ( + AuctionBidParams, + AuctionCloseParams, + AuctionOpenParams, + BuyRequestUser (..), + Content (..), + InitParams (..), + MintParams (..), + NftAppInstance (..), + NftId (..), + SetPriceParams (..), + Title (..), + UniqueToken, + UserId (..), + appInstance'UniqueToken, + getUserId, + ) +import Mlabs.Utils.Wallet (walletFromNumber) + +-- | Wallets that are used for testing. +w1, w2, w3, wA :: Wallet +w1 = walletFromNumber 1 +w2 = walletFromNumber 2 +w3 = walletFromNumber 3 +wA = walletFromNumber 4 -- Admin Wallet + +{- it was 2 before, but after switching + `Plutus.Contracts.Currency.mintContract` + to `Mlabs.Plutus.Contracts.Currency.mintContract` + tests start to fail with 2 slots waiting +-} +waitInit :: EmulatorTrace () +waitInit = void $ waitNSlots 3 + +-- | Calls initialisation of state for Nft pool +callStartNft :: Wallet -> EmulatorTrace NftAppInstance +callStartNft wal = do + hAdmin <- activateContractWallet wal adminEndpoints + let params = + InitParams + [UserId . toSpooky . mockWalletPaymentPubKeyHash $ wal] + (R.reduce 5 1000) + (unPaymentPubKeyHash . toSpookyPaymentPubKeyHash $ mockWalletPaymentPubKeyHash wal) + callEndpoint @"app-init" hAdmin params + waitInit + oState <- observableState hAdmin + appInstance <- case getLast oState of + Nothing -> throwError $ GenericError "App Symbol Could not be established." + Just aS -> pure aS + void $ waitNSlots 1 + pure appInstance + +callStartNftFail :: Wallet -> ScriptM () +callStartNftFail wal = do + let w5 = walletFromNumber 5 + params = + InitParams + [UserId . toSpooky . mockWalletPaymentPubKeyHash $ w5] + (R.reduce 5 1000) + (unPaymentPubKeyHash . toSpookyPaymentPubKeyHash $ mockWalletPaymentPubKeyHash wal) + lift $ do + hAdmin <- activateContractWallet wal adminEndpoints + callEndpoint @"app-init" hAdmin params + waitInit + +type ScriptM a = + ReaderT + UniqueToken + ( Eff + '[ StartContract + , RunContract + , Assert + , Waiting + , EmulatorControl + , EmulatedWalletAPI + , LogMsg String + , Error EmulatorRuntimeError + ] + ) + a + +type Script = ScriptM () + +checkOptions :: CheckOptions +checkOptions = defaultCheckOptions & emulatorConfig . initialChainState .~ Left initialDistribution + +toUserId :: Wallet -> UserId +toUserId = UserId . toSpooky . mockWalletPaymentPubKeyHash + +{- | Script runner. It inits NFT by user 1 and provides nft id to all sequent + endpoint calls. +-} +runScript :: Wallet -> Script -> EmulatorTrace () +runScript wal script = do + appInstance <- callStartNft wal + next + runReaderT script $ appInstance'UniqueToken appInstance + +userMint :: Wallet -> MintParams -> ScriptM NftId +userMint wal mp = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (endpoints symbol) + callEndpoint @"mint" hdl mp + next + oState <- observableState hdl + case findNftId oState of + Nothing -> throwError $ GenericError "Could not mint NFT" + Just nftId -> pure nftId + where + findNftId :: forall a b. Last (Either a b) -> Maybe a + findNftId x = case getLast x of + Just (Left x') -> Just x' + _ -> Nothing + +userSetPrice :: Wallet -> SetPriceParams -> Script +userSetPrice wal sp = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (endpoints symbol) + callEndpoint @"set-price" hdl sp + next + +userBuy :: Wallet -> BuyRequestUser -> Script +userBuy wal br = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (endpoints symbol) + callEndpoint @"buy" hdl br + next + +userQueryPrice :: Wallet -> NftId -> Script +userQueryPrice wal nftId = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (queryEndpoints symbol) + callEndpoint @"query-current-price" hdl nftId + +userQueryOwner :: Wallet -> NftId -> Script +userQueryOwner wal nftId = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (queryEndpoints symbol) + callEndpoint @"query-current-owner" hdl nftId + +userQueryListNfts :: Wallet -> Script +userQueryListNfts wal = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (queryEndpoints symbol) + callEndpoint @"query-list-nfts" hdl () + +userQueryContent :: Wallet -> Content -> Script +userQueryContent wal content = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (queryEndpoints symbol) + callEndpoint @"query-content" hdl content + +userStartAuction :: Wallet -> AuctionOpenParams -> Script +userStartAuction wal params = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (endpoints symbol) + callEndpoint @"auction-open" hdl params + next + +userBidAuction :: Wallet -> AuctionBidParams -> Script +userBidAuction wal params = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (endpoints symbol) + callEndpoint @"auction-bid" hdl params + next + +userCloseAuction :: Wallet -> AuctionCloseParams -> Script +userCloseAuction wal params = do + symbol <- ask + lift $ do + hdl <- activateContractWallet wal (endpoints symbol) + callEndpoint @"auction-close" hdl params + next + +userWait :: Natural -> Script +userWait = lift . void . waitNSlots + +{- | Initial distribution of wallets for testing. + We have 3 users. All of them get 1000 lovelace at the start. +-} +initialDistribution :: M.Map Wallet Plutus.V1.Ledger.Value.Value +initialDistribution = + M.fromList + [ (w1, val 1000_000_000) + , (w2, val 1000_000_000) + , (w3, val 1000_000_000) + , (wA, val 1000_000_000) + ] + where + val x = singleton adaSymbol adaToken x + +-- | Check if wallet contains Ada +ownsAda :: Wallet -> Integer -> Scene +ownsAda wal amount = wal `owns` [(adaCoin, amount)] + +check :: String -> TracePredicate -> Wallet -> Script -> TestTree +check msg assertions wal script = checkPredicateOptions checkOptions msg assertions (runScript wal script) + +-- | Scene without any transfers +noChangesScene :: Scene +noChangesScene = foldMap (`ownsAda` 0) [w1, w2, w3] + +containsLog :: Wallet -> String -> TracePredicate +containsLog wal expected = assertInstanceLog (walletInstanceTag wal) (any predicate) + where + predicate = \case + (EmulatorTimeEvent _ (ContractInstanceLog (ContractLog (String actual)) _ _)) -> + T.pack expected Hask.== actual + _ -> False + +artwork1 :: MintParams +artwork1 = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Nothing + } + +artwork2 :: MintParams +artwork2 = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "Another painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 300 + } + +mkFreeGov :: Wallet -> Integer -> Plutus.V1.Ledger.Value.Value +mkFreeGov wal = assetClassValue (AssetClass (govCurrency, tn)) + where + tn = TokenName . ("freeGov" <>) . getPubKeyHash . unPaymentPubKeyHash . getUserId . toUserId $ wal + +govCurrency :: CurrencySymbol +govCurrency = "004feb05c46d98d379322a9563b717cdc8b5c872f2f7f2bc210f994d" + +getFreeGov :: Wallet -> Plutus.V1.Ledger.Value.Value -> Integer +getFreeGov wal val = valueOf val govCurrency tn + where + tn = TokenName . ("freeGov" <>) . getPubKeyHash . unPaymentPubKeyHash . getUserId . toUserId $ wal + +appSymbol :: UniqueToken +appSymbol = toSpookyAssetClass $ AssetClass ("038ecf2f85dcb99b41d7ebfcbc0d988f4ac2971636c3e358aa8d6121", "Unique App Token") diff --git a/mlabs/test/Test/NFT/QuickCheck.hs b/mlabs/test/Test/NFT/QuickCheck.hs new file mode 100644 index 000000000..8753312c9 --- /dev/null +++ b/mlabs/test/Test/NFT/QuickCheck.hs @@ -0,0 +1,489 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StrictData #-} + +module Test.NFT.QuickCheck where + +import Control.Lens (at, makeLenses, set, view, (^.)) +import Control.Monad (forM_, void, when) +import Data.Default (def) +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.Monoid (Last (..)) +import Data.String (IsString (..)) +import Data.Text (Text) +import Ledger.TimeSlot (slotToBeginPOSIXTime) +import Plutus.Contract.Test (Wallet (..), mockWalletPaymentPubKeyHash) +import Plutus.Contract.Test.ContractModel ( + Action, + Actions, + ContractInstanceSpec (..), + ContractModel (..), + DL, + action, + anyActions_, + assertModel, + contractState, + currentSlot, + defaultCoverageOptions, + deposit, + forAllDL, + getModelState, + lockedValue, + propRunActionsWithOptions, + transfer, + viewContractState, + wait, + withdraw, + ($=), + ($~), + ) +import Plutus.Trace.Emulator (callEndpoint) +import Plutus.Trace.Emulator qualified as Trace +import Plutus.V1.Ledger.Ada (lovelaceValueOf) +import Plutus.V1.Ledger.Slot (Slot (..)) +import Plutus.V1.Ledger.Value (valueOf) +import PlutusTx.Prelude hiding ((<$>), (<*>), (==)) +import PlutusTx.Ratio qualified as R +import Test.QuickCheck qualified as QC +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.QuickCheck (testProperty) +import Prelude ((<$>), (<*>), (==)) +import Prelude qualified as Hask + +import Ledger (PaymentPubKeyHash (unPaymentPubKeyHash)) +import Mlabs.NFT.Api (NFTAppSchema, adminEndpoints, endpoints) +import Mlabs.NFT.Contract (hashData) +import Mlabs.NFT.Spooky (toSpooky, toSpookyPubKeyHash, unSpookyValue) +import Mlabs.NFT.Types ( + AuctionBidParams (..), + AuctionCloseParams (..), + AuctionOpenParams (..), + BuyRequestUser (..), + Content (..), + InitParams (..), + MintParams (..), + NftAppInstance, + NftId (..), + QueryResponse, + SetPriceParams (..), + Title (..), + ) +import Mlabs.NFT.Validation (calculateShares) +import Test.NFT.Init (appSymbol, checkOptions, mkFreeGov, toUserId, w1, w2, w3, wA) + +data MockAuctionState = MockAuctionState + { _auctionHighestBid :: Maybe (Integer, Wallet) + , _auctionDeadline :: Slot + , _auctionMinBid :: Integer + } + deriving (Hask.Show, Hask.Eq) +makeLenses ''MockAuctionState + +-- Mock content for overriding Show instance, to show hash instead, useful for debugging +newtype MockContent = MockContent {getMockContent :: Content} + deriving (Hask.Eq) + +instance Hask.Show MockContent where + show x = Hask.show (hashData . getMockContent $ x, getMockContent x) + +-- We cannot use InformationNft because we need access to `Wallet` +-- `PubKeyHash` is not enough for simulation +data MockNft = MockNft + { _nftId :: NftId + , _nftPrice :: Maybe Integer + , _nftOwner :: Wallet + , _nftAuthor :: Wallet + , _nftShare :: Rational + , _nftAuctionState :: Maybe MockAuctionState + } + deriving (Hask.Show, Hask.Eq) +makeLenses ''MockNft + +data NftModel = NftModel + { _mMarket :: Map NftId MockNft + , _mMintedCount :: Integer + , _mStarted :: Bool + } + deriving (Hask.Show, Hask.Eq) +makeLenses ''NftModel + +instance ContractModel NftModel where + data Action NftModel + = ActionInit + | ActionMint + { aPerformer :: Wallet + , aContent :: MockContent + , aTitle :: Title + , aNewPrice :: Maybe Integer + , aShare :: Rational + } + | ActionSetPrice + { aPerformer :: Wallet + , aNftId :: ~NftId + , aNewPrice :: Maybe Integer + } + | ActionBuy + { aPerformer :: Wallet + , aNftId :: ~NftId + , aPrice :: Integer + , aNewPrice :: Maybe Integer + } + | ActionAuctionOpen + { aPerformer :: Wallet + , aNftId :: ~NftId + , aDeadline :: Slot + , aMinBid :: Integer + } + | ActionAuctionBid + { aPerformer :: Wallet + , aNftId :: ~NftId + , aBid :: Integer + } + | ActionAuctionClose + { aPerformer :: Wallet + , aNftId :: ~NftId + } + | ActionWait -- This action should not be generated (do NOT add it to arbitraryAction) + { aWaitSlots :: Integer + } + deriving (Hask.Show, Hask.Eq) + + data ContractInstanceKey NftModel w s e where + InitKey :: Wallet -> ContractInstanceKey NftModel (Last NftAppInstance) NFTAppSchema Text + UserKey :: Wallet -> ContractInstanceKey NftModel (Last (Either NftId QueryResponse)) NFTAppSchema Text + + instanceTag key _ = fromString $ Hask.show key + + initialHandleSpecs = instanceSpec + + arbitraryAction model = + let nfts = Map.keys (model ^. contractState . mMarket) + genWallet = QC.elements wallets + genNonNeg = ((* 1_000_000) . (+ 1)) . QC.getNonNegative <$> QC.arbitrary + genDeadline = Slot . QC.getNonNegative <$> QC.arbitrary + -- genDeadline = Hask.pure @QC.Gen (Slot 9999) + genMaybePrice = QC.oneof [Hask.pure Nothing, Just <$> genNonNeg] + genString = QC.listOf (QC.elements [Hask.minBound .. Hask.maxBound]) + genContent = MockContent . Content . toSpooky @BuiltinByteString . fromString . ('x' :) <$> genString + -- genTitle = Title . fromString <$> genString + genTitle = Hask.pure (Title . toSpooky @BuiltinByteString $ "") + genShare = (`R.reduce` 100) <$> QC.elements [1 .. 99] + genNftId = QC.elements nfts + in QC.oneof + [ Hask.pure ActionInit + , ActionMint + <$> genWallet + <*> genContent + <*> genTitle + <*> genMaybePrice + <*> genShare + , ActionSetPrice + <$> genWallet + <*> genNftId + <*> genMaybePrice + , ActionBuy + <$> genWallet + <*> genNftId + <*> genNonNeg + <*> genMaybePrice + -- , ActionAuctionOpen + -- <$> genWallet + -- <*> genNftId + -- <*> genDeadline + -- <*> genNonNeg + -- , ActionAuctionBid + -- <$> genWallet + -- <*> genNftId + -- <*> genNonNeg + -- , ActionAuctionClose + -- <$> genWallet + -- <*> genNftId + ] + + initialState = NftModel Map.empty 0 False + + precondition s ActionInit {} = not (s ^. contractState . mStarted) + precondition s ActionMint {..} = + (s ^. contractState . mStarted) + && (s ^. contractState . mMintedCount <= 5) + && not (Map.member (NftId . toSpooky . hashData . getMockContent $ aContent) (s ^. contractState . mMarket)) + precondition s ActionBuy {..} = + (s ^. contractState . mStarted) + && (s ^. contractState . mMintedCount > 0) + && isJust ((s ^. contractState . mMarket . at aNftId) >>= _nftPrice) + && (Just aPrice >= ((s ^. contractState . mMarket . at aNftId) >>= _nftPrice)) + precondition s ActionSetPrice {..} = + (s ^. contractState . mStarted) + && (s ^. contractState . mMintedCount > 0) + && (Just aPerformer == (view nftOwner <$> (s ^. contractState . mMarket . at aNftId))) + && isNothing ((s ^. contractState . mMarket . at aNftId) >>= _nftAuctionState) + precondition s ActionAuctionOpen {..} = + (s ^. contractState . mStarted) + && (s ^. contractState . mMintedCount > 0) + && (Just aPerformer == (view nftOwner <$> (s ^. contractState . mMarket . at aNftId))) + && isNothing ((s ^. contractState . mMarket . at aNftId) >>= _nftAuctionState) + precondition s ActionAuctionBid {..} = + (s ^. contractState . mStarted) + && (s ^. contractState . mMintedCount > 0) + && isJust ((s ^. contractState . mMarket . at aNftId) >>= _nftAuctionState) + && (Just aBid > (view auctionMinBid <$> ((s ^. contractState . mMarket . at aNftId) >>= _nftAuctionState))) + && (Just aBid > (fst <$> ((s ^. contractState . mMarket . at aNftId) >>= _nftAuctionState >>= _auctionHighestBid))) + && (Just (s ^. currentSlot + 1) < (view auctionDeadline <$> ((s ^. contractState . mMarket . at aNftId) >>= _nftAuctionState))) + precondition s ActionAuctionClose {..} = + (s ^. contractState . mStarted) + && (s ^. contractState . mMintedCount > 0) + && isJust ((s ^. contractState . mMarket . at aNftId) >>= _nftAuctionState) + && (Just (s ^. currentSlot) > (view auctionDeadline <$> ((s ^. contractState . mMarket . at aNftId) >>= _nftAuctionState))) + precondition _ ActionWait {} = True + + nextState ActionInit {} = do + mStarted $= True + wait 5 + nextState ActionMint {..} = do + s <- view contractState <$> getModelState + let nft = + MockNft + { _nftId = NftId . toSpooky . hashData . getMockContent $ aContent + , _nftPrice = aNewPrice + , _nftOwner = aPerformer + , _nftAuthor = aPerformer + , _nftShare = aShare + , _nftAuctionState = Nothing + } + let nft' = s ^. mMarket . at (nft ^. nftId) + case nft' of + Nothing -> do + mMarket $~ Map.insert (nft ^. nftId) nft + mMintedCount $~ (+ 1) + Just _ -> Hask.pure () -- NFT is already minted + wait 5 + nextState ActionSetPrice {..} = do + s <- view contractState <$> getModelState + let nft' = s ^. mMarket . at aNftId + case nft' of + Nothing -> Hask.pure () -- NFT not found + Just nft -> do + when ((nft ^. nftOwner) == aPerformer && isNothing (nft ^. nftAuctionState)) $ do + let newNft = set nftPrice aNewPrice nft + mMarket $~ Map.insert aNftId newNft + wait 5 + nextState ActionBuy {..} = do + s <- view contractState <$> getModelState + let nft' = s ^. mMarket . at aNftId + case nft' of + Nothing -> Hask.pure () -- NFT not found + Just nft -> case nft ^. nftPrice of + Nothing -> Hask.pure () -- NFT is locked + Just nftPrice' -> do + when (nftPrice' <= aPrice) $ do + let newNft = set nftOwner aPerformer . set nftPrice aNewPrice $ nft + feeValue = round $ fromInteger aPrice * feeRate + (ownerShare, authorShare) = calculateShares (aPrice - feeValue) (nft ^. nftShare) + mMarket $~ Map.insert aNftId newNft + transfer aPerformer (nft ^. nftOwner) (unSpookyValue ownerShare) + transfer aPerformer (nft ^. nftAuthor) (unSpookyValue authorShare) + transfer aPerformer wAdmin (lovelaceValueOf feeValue) + deposit aPerformer (mkFreeGov aPerformer feeValue) + wait 5 + nextState ActionAuctionOpen {..} = do + s <- view contractState <$> getModelState + let nft' = s ^. mMarket . at aNftId + case nft' of + Nothing -> Hask.pure () -- NFT not found + Just nft -> do + when ((nft ^. nftOwner) == aPerformer && isNothing (nft ^. nftAuctionState)) $ do + let ac = + MockAuctionState + { _auctionHighestBid = Nothing + , _auctionDeadline = aDeadline + , _auctionMinBid = aMinBid + } + newNft = + set nftAuctionState (Just ac) $ + set nftPrice Nothing nft + mMarket $~ Map.insert aNftId newNft + wait 5 + nextState ActionAuctionBid {..} = do + s <- view contractState <$> getModelState + curSlot <- view currentSlot <$> getModelState + let nft' = s ^. mMarket . at aNftId + case nft' of + Nothing -> Hask.pure () -- NFT not found + Just nft -> case nft ^. nftAuctionState of + Nothing -> Hask.pure () -- NFT not on auction + Just ac -> do + when (ac ^. auctionDeadline > curSlot) $ do + let newAc = set auctionHighestBid (Just (aBid, aPerformer)) ac + newNft = set nftAuctionState (Just newAc) nft + case ac ^. auctionHighestBid of + Nothing -> do + -- First bid + when (ac ^. auctionMinBid <= aBid) $ do + mMarket $~ Map.insert aNftId newNft + withdraw aPerformer (lovelaceValueOf aBid) + Just hb -> + -- Next bid + when (fst hb < aBid) $ do + mMarket $~ Map.insert aNftId newNft + deposit (snd hb) (lovelaceValueOf . fst $ hb) + withdraw aPerformer (lovelaceValueOf aBid) + wait 10 + nextState ActionAuctionClose {..} = do + s <- view contractState <$> getModelState + curSlot <- view currentSlot <$> getModelState + let nft' = s ^. mMarket . at aNftId + case nft' of + Nothing -> Hask.pure () -- NFT not found + Just nft -> case nft ^. nftAuctionState of + Nothing -> Hask.pure () -- NFT not on auction + Just ac -> do + when (ac ^. auctionDeadline < curSlot) $ do + case ac ^. auctionHighestBid of + Nothing -> do + -- No bids + let newNft = set nftAuctionState Nothing nft + mMarket $~ Map.insert aNftId newNft + Just hb -> do + let newOwner = snd hb + newNft = set nftOwner newOwner $ set nftAuctionState Nothing nft + price = fst hb + feeValue = round $ fromInteger price * feeRate + (ownerShare, authorShare) = calculateShares (price - feeValue) (nft ^. nftShare) + mMarket $~ Map.insert aNftId newNft + deposit (nft ^. nftOwner) (unSpookyValue ownerShare) + deposit (nft ^. nftAuthor) (unSpookyValue authorShare) + deposit wAdmin (lovelaceValueOf feeValue) + deposit newOwner (mkFreeGov newOwner feeValue) + wait 5 + nextState ActionWait {..} = do + wait aWaitSlots + + perform h _ = \case + ActionInit -> do + let hAdmin = h $ InitKey wAdmin + params = + InitParams + [toUserId wAdmin] + (R.reduce 5 1000) + (toSpookyPubKeyHash . unPaymentPubKeyHash . mockWalletPaymentPubKeyHash $ wAdmin) + callEndpoint @"app-init" hAdmin params + void $ Trace.waitNSlots 5 + ActionMint {..} -> do + let h1 = h $ UserKey aPerformer + callEndpoint @"mint" h1 $ + MintParams + { mp'content = getMockContent aContent + , mp'title = aTitle + , mp'share = aShare + , mp'price = aNewPrice + } + void $ Trace.waitNSlots 5 + ActionSetPrice {..} -> do + let h1 = h $ UserKey aPerformer + callEndpoint @"set-price" h1 $ + SetPriceParams + { sp'nftId = aNftId + , sp'price = aNewPrice + } + void $ Trace.waitNSlots 5 + ActionBuy {..} -> do + let h1 = h $ UserKey aPerformer + callEndpoint @"buy" h1 $ + BuyRequestUser + { ur'nftId = aNftId + , ur'newPrice = aNewPrice + , ur'price = aPrice + } + void $ Trace.waitNSlots 5 + ActionAuctionOpen {..} -> do + let h1 = h $ UserKey aPerformer + callEndpoint @"auction-open" h1 $ + AuctionOpenParams + { op'nftId = aNftId + , op'deadline = slotToBeginPOSIXTime def aDeadline + , op'minBid = aMinBid + } + void $ Trace.waitNSlots 5 + ActionAuctionBid {..} -> do + let h1 = h $ UserKey aPerformer + callEndpoint @"auction-bid" h1 $ + AuctionBidParams + { bp'nftId = aNftId + , bp'bidAmount = aBid + } + void $ Trace.waitNSlots 9 + callEndpoint @"query-list-nfts" h1 () + void $ Trace.waitNSlots 1 + ActionAuctionClose {..} -> do + let h1 = h $ UserKey aPerformer + callEndpoint @"auction-close" h1 $ + AuctionCloseParams + { cp'nftId = aNftId + } + void $ Trace.waitNSlots 5 + ActionWait {..} -> do + void . Trace.waitNSlots . Hask.fromInteger $ aWaitSlots + +deriving instance Hask.Eq (ContractInstanceKey NftModel w s e) +deriving instance Hask.Show (ContractInstanceKey NftModel w s e) + +feeRate :: Rational +feeRate = R.reduce 5 1000 + +wallets :: [Wallet] +wallets = [w1, w2, w3] + +wAdmin :: Wallet +wAdmin = wA + +instanceSpec :: [ContractInstanceSpec NftModel] +instanceSpec = + [ ContractInstanceSpec (InitKey wAdmin) wAdmin adminEndpoints + ] + <> Hask.fmap (\w -> ContractInstanceSpec (UserKey w) w (endpoints appSymbol)) wallets + +propContract :: Actions NftModel -> QC.Property +propContract = + -- HACK + -- 10 test runs execute relatively quickly, which we can then + -- run multiple times in 'test/Main.hs' + QC.withMaxSuccess 10 + . propRunActionsWithOptions + checkOptions + defaultCoverageOptions + (const $ Hask.pure True) + +noLockedFunds :: DL NftModel () +noLockedFunds = do + action ActionInit + anyActions_ + + nfts <- viewContractState mMarket + + -- Wait for all auctions to end + forM_ nfts $ \nft -> do + case nft ^. nftAuctionState of + Just as -> do + action $ ActionWait $ getSlot (as ^. auctionDeadline) + Nothing -> Hask.pure () + + -- Close all auctions + forM_ nfts $ \nft -> do + case nft ^. nftAuctionState of + Just _ -> do + action $ ActionAuctionClose w1 (nft ^. nftId) + Nothing -> Hask.pure () + + assertModel "Locked funds should be zero" $ (== 0) . (\v -> valueOf v "" "") . lockedValue + +propNoLockedFunds :: QC.Property +propNoLockedFunds = QC.withMaxSuccess 10 $ forAllDL noLockedFunds propContract + +test :: TestTree +test = + testGroup + "QuickCheck" + [ testProperty "Can get funds out" propNoLockedFunds + , testProperty "Contract" propContract + ] diff --git a/mlabs/test/Test/NFT/Script/Auction.hs b/mlabs/test/Test/NFT/Script/Auction.hs new file mode 100644 index 000000000..38df26ee1 --- /dev/null +++ b/mlabs/test/Test/NFT/Script/Auction.hs @@ -0,0 +1,374 @@ +module Test.NFT.Script.Auction ( + testAuctionBeforeDeadline, + testAuctionAfterDeadline, +) where + +import Data.Default (def) +import Data.Semigroup ((<>)) +import Ledger qualified +import Ledger.TimeSlot (slotToBeginPOSIXTime) +import Mlabs.NFT.Types qualified as NFT +import Mlabs.NFT.Validation qualified as NFT +import Plutus.V1.Ledger.Interval qualified as Interval +import PlutusTx qualified +import PlutusTx.Prelude hiding ((<>)) +import PlutusTx.Prelude qualified as PlutusPrelude +import Test.NFT.Script.Values as TestValues +import Test.Tasty (TestTree, localOption) +import Test.Tasty.Plutus.Context +import Test.Tasty.Plutus.Script.Unit + +slotFiveTime :: Ledger.POSIXTime +slotFiveTime = slotToBeginPOSIXTime def 5 + +slotTenTime :: Ledger.POSIXTime +slotTenTime = slotToBeginPOSIXTime def 10 + +slotElevenTime :: Ledger.POSIXTime +slotElevenTime = slotToBeginPOSIXTime def 11 + +testAuctionBeforeDeadline :: TestTree +testAuctionBeforeDeadline = error () + +-- testAuctionBeforeDeadline = localOption (TestTxId TestValues.testTxId) $ +-- localOption (TimeRange $ Interval.to slotFiveTime) $ +-- withValidator "Test NFT dealing validator (auction before deadline)" dealingValidator $ do +-- shouldn'tValidate "Author can't close auction if not owner" closeAuctionData1 closeAuctionContext1 +-- shouldValidate "Owner can start auction" validOpenAuctionData validOpenAuctionContext +-- shouldn'tValidate "Owner can't close auction before deadline" validCloseAuctionData validCloseAuctionContext +-- shouldValidate "Can bid before deadline" validBidData validBidContext +-- shouldValidate "Can make higher bid" validSecondBidData validSecondBidContext + +testAuctionAfterDeadline :: TestTree +testAuctionAfterDeadline = error () + +-- testAuctionAfterDeadline = localOption (TimeRange $ Interval.from slotElevenTime) $ +-- withValidator "Test NFT dealing validator (auction after deadline)" dealingValidator $ do +-- shouldValidate "Owner can close auction" validCloseAuctionData validCloseAuctionContext +-- shouldn'tValidate "Can't bid after deadline" validBidData validBidContext +-- shouldValidate "Can close auction with a bid" closeAuctionWithBidData closeAuctionWithBidContext +-- shouldn'tValidate "Can't close auction if author not paid" closeAuctionWithBidData closeAuctionWithBidNoAuthorContext +-- shouldn'tValidate "Can't close auction if owner not paid" closeAuctionWithBidData closeAuctionWithBidNoOwnerContext +-- shouldn'tValidate "Can't close auction if owner=author not paid" closeAuctionWithBidAuthorData closeAuctionWithBidAuthorContext +-- shouldn'tValidate "Can't close auction if datum illegaly altered" closeAuctionInconsistentData closeAuctionInconsistentContext + +-- initialNode :: NFT.NftListNode +-- initialNode = +-- NFT.NftListNode +-- { node'information = +-- NFT.InformationNft +-- { info'id = TestValues.testNftId +-- , info'share = 1 % 2 +-- , info'author = NFT.UserId TestValues.authorPkh +-- , info'owner = NFT.UserId TestValues.authorPkh +-- , info'price = Nothing +-- , info'auctionState = Nothing +-- } +-- , node'next = Nothing +-- , node'appInstance = TestValues.appInstance +-- } + +-- initialAuthorDatum :: NFT.DatumNft +-- initialAuthorDatum = NFT.NodeDatum initialNode + +-- ownerUserOneNode :: NFT.NftListNode +-- ownerUserOneNode = +-- initialNode +-- { NFT.node'information = +-- (NFT.node'information initialNode) +-- { NFT.info'owner = NFT.UserId TestValues.userOnePkh +-- } +-- } + +-- ownerUserOneDatum :: NFT.DatumNft +-- ownerUserOneDatum = +-- NFT.NodeDatum ownerUserOneNode + +-- openAuctionState :: NFT.AuctionState +-- openAuctionState = +-- NFT.AuctionState +-- { as'highestBid = Nothing +-- , as'deadline = slotTenTime +-- , as'minBid = 100 * 1_000_000 +-- } + +-- bidAuctionState :: NFT.AuctionState +-- bidAuctionState = +-- NFT.AuctionState +-- { as'highestBid = Just (NFT.AuctionBid (300 * 1_000_000) (NFT.UserId TestValues.userTwoPkh)) +-- , as'deadline = slotTenTime +-- , as'minBid = 100 * 1_000_000 +-- } + +-- secondBidAuctionState :: NFT.AuctionState +-- secondBidAuctionState = +-- NFT.AuctionState +-- { as'highestBid = Just (NFT.AuctionBid (500 * 1_000_000) (NFT.UserId TestValues.userThreePkh)) +-- , as'deadline = slotTenTime +-- , as'minBid = 100 * 1_000_000 +-- } + +-- ownerUserOneAuctionOpenDatum :: NFT.DatumNft +-- ownerUserOneAuctionOpenDatum = +-- NFT.NodeDatum $ +-- ownerUserOneNode +-- { NFT.node'information = +-- (NFT.node'information ownerUserOneNode) +-- { NFT.info'auctionState = Just openAuctionState +-- } +-- } + +-- ownerUserOneAuctionBidDatum :: NFT.DatumNft +-- ownerUserOneAuctionBidDatum = +-- NFT.NodeDatum $ +-- ownerUserOneNode +-- { NFT.node'information = +-- (NFT.node'information ownerUserOneNode) +-- { NFT.info'auctionState = Just bidAuctionState +-- } +-- } + +-- ownerUserOneAuctionSecondBidDatum :: NFT.DatumNft +-- ownerUserOneAuctionSecondBidDatum = +-- NFT.NodeDatum $ +-- ownerUserOneNode +-- { NFT.node'information = +-- (NFT.node'information ownerUserOneNode) +-- { NFT.info'auctionState = Just secondBidAuctionState +-- } +-- } + +-- auctionWithBidCloseDatum :: NFT.DatumNft +-- auctionWithBidCloseDatum = +-- NFT.NodeDatum $ +-- ownerUserOneNode +-- { NFT.node'information = +-- (NFT.node'information ownerUserOneNode) +-- { NFT.info'owner = NFT.UserId TestValues.userTwoPkh +-- } +-- } + +-- auctionWithBidAuthorNode :: NFT.NftListNode +-- auctionWithBidAuthorNode = +-- initialNode +-- { NFT.node'information = +-- (NFT.node'information initialNode) +-- { NFT.info'auctionState = Just bidAuctionState +-- } +-- } + +-- auctionWithBidAuthorDatum :: NFT.DatumNft +-- auctionWithBidAuthorDatum = +-- NFT.NodeDatum auctionWithBidAuthorNode + +-- auctionCloseInconsistentDatum :: NFT.DatumNft +-- auctionCloseInconsistentDatum = +-- NFT.NodeDatum $ +-- auctionWithBidAuthorNode +-- { NFT.node'information = +-- (NFT.node'information auctionWithBidAuthorNode) +-- { NFT.info'auctionState = Nothing +-- , NFT.info'author = NFT.UserId TestValues.userOnePkh +-- , NFT.info'owner = NFT.UserId TestValues.userTwoPkh +-- } +-- } + +-- -- case 1 +-- openAuctionData1 :: TestData 'ForSpending +-- openAuctionData1 = SpendingTest dtm redeemer val +-- where +-- dtm = ownerUserOneDatum + +-- redeemer = +-- NFT.OpenAuctionAct +-- { act'symbol = TestValues.appSymbol +-- } + +-- val = TestValues.oneNft + +-- openAuctionContext1 :: ContextBuilder 'ForSpending +-- openAuctionContext1 = +-- paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft ownerUserOneAuctionOpenDatum +-- <> paysSelf mempty ownerUserOneAuctionOpenDatum +-- <> includeGovHead + +-- -- case 2 +-- closeAuctionData1 :: TestData 'ForSpending +-- closeAuctionData1 = SpendingTest dtm redeemer val +-- where +-- dtm = ownerUserOneAuctionOpenDatum + +-- redeemer = +-- NFT.CloseAuctionAct +-- { act'symbol = TestValues.appSymbol +-- } + +-- val = TestValues.oneNft + +-- closeAuctionContext1 :: ContextBuilder 'ForSpending +-- closeAuctionContext1 = +-- paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft ownerUserOneDatum +-- <> paysSelf mempty ownerUserOneDatum +-- <> includeGovHead + +-- -- case 3 +-- validOpenAuctionData :: TestData 'ForSpending +-- validOpenAuctionData = SpendingTest dtm redeemer val +-- where +-- dtm = ownerUserOneDatum + +-- redeemer = +-- NFT.OpenAuctionAct +-- { act'symbol = TestValues.appSymbol +-- } + +-- val = TestValues.oneNft + +-- validOpenAuctionContext :: ContextBuilder 'ForSpending +-- validOpenAuctionContext = +-- paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft ownerUserOneAuctionOpenDatum +-- <> signedWith userOnePkh +-- <> includeGovHead + +-- -- case 4 +-- validCloseAuctionData :: TestData 'ForSpending +-- validCloseAuctionData = SpendingTest dtm redeemer val +-- where +-- dtm = ownerUserOneAuctionOpenDatum + +-- redeemer = +-- NFT.CloseAuctionAct +-- { act'symbol = TestValues.appSymbol +-- } + +-- val = TestValues.oneNft + +-- validCloseAuctionContext :: ContextBuilder 'ForSpending +-- validCloseAuctionContext = +-- paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft ownerUserOneDatum +-- <> signedWith userOnePkh +-- <> includeGovHead + +-- validBidData :: TestData 'ForSpending +-- validBidData = SpendingTest dtm redeemer val +-- where +-- dtm = ownerUserOneAuctionOpenDatum + +-- redeemer = +-- NFT.BidAuctionAct +-- { act'bid = 300 * 1_000_000 +-- , act'symbol = TestValues.appSymbol +-- } + +-- val = TestValues.oneNft + +-- validBidContext :: ContextBuilder 'ForSpending +-- validBidContext = +-- paysToOther (NFT.txValHash uniqueAsset) (TestValues.oneNft <> TestValues.adaValue 300) ownerUserOneAuctionBidDatum +-- <> includeGovHead + +-- validSecondBidData :: TestData 'ForSpending +-- validSecondBidData = SpendingTest dtm redeemer val +-- where +-- dtm = ownerUserOneAuctionBidDatum + +-- redeemer = +-- NFT.BidAuctionAct +-- { act'bid = 500 * 1_000_000 +-- , act'symbol = TestValues.appSymbol +-- } + +-- val = TestValues.oneNft <> TestValues.adaValue 300 + +-- validSecondBidContext :: ContextBuilder 'ForSpending +-- validSecondBidContext = +-- paysToOther (NFT.txValHash uniqueAsset) (TestValues.oneNft PlutusPrelude.<> TestValues.adaValue 500) ownerUserOneAuctionSecondBidDatum +-- <> paysToWallet TestValues.userTwoWallet (TestValues.adaValue 300) +-- <> includeGovHead + +-- closeAuctionWithBidData :: TestData 'ForSpending +-- closeAuctionWithBidData = SpendingTest dtm redeemer val +-- where +-- dtm = ownerUserOneAuctionBidDatum + +-- redeemer = +-- NFT.CloseAuctionAct +-- { act'symbol = TestValues.appSymbol +-- } + +-- -- TODO: correctInputValue check for all redeemers? +-- val = TestValues.oneNft -- <> (TestValues.adaValue 300) + +-- closeAuctionWithBidContext :: ContextBuilder 'ForSpending +-- closeAuctionWithBidContext = +-- paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft auctionWithBidCloseDatum +-- <> signedWith userOnePkh +-- <> paysToWallet TestValues.authorWallet (TestValues.adaValue 150) +-- <> paysToWallet TestValues.userOneWallet (TestValues.adaValue 150) +-- <> includeGovHead + +-- closeAuctionWithBidNoAuthorContext :: ContextBuilder 'ForSpending +-- closeAuctionWithBidNoAuthorContext = +-- paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft auctionWithBidCloseDatum +-- <> paysSelf mempty auctionWithBidCloseDatum +-- <> signedWith userOnePkh +-- <> paysToWallet TestValues.userOneWallet (TestValues.adaValue 150) +-- <> includeGovHead + +-- closeAuctionWithBidNoOwnerContext :: ContextBuilder 'ForSpending +-- closeAuctionWithBidNoOwnerContext = +-- paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft auctionWithBidCloseDatum +-- <> paysSelf mempty auctionWithBidCloseDatum +-- <> signedWith userOnePkh +-- <> paysToWallet TestValues.authorWallet (TestValues.adaValue 150) +-- <> includeGovHead + +-- closeAuctionWithBidAuthorData :: TestData 'ForSpending +-- closeAuctionWithBidAuthorData = SpendingTest dtm redeemer val +-- where +-- dtm = auctionWithBidAuthorDatum + +-- redeemer = +-- NFT.CloseAuctionAct +-- { act'symbol = TestValues.appSymbol +-- } + +-- val = TestValues.oneNft + +-- closeAuctionWithBidAuthorContext :: ContextBuilder 'ForSpending +-- closeAuctionWithBidAuthorContext = +-- paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft auctionWithBidCloseDatum +-- <> paysSelf mempty auctionWithBidCloseDatum +-- <> signedWith authorPkh +-- <> paysToWallet TestValues.authorWallet (TestValues.adaValue 150) +-- <> includeGovHead + +-- closeAuctionInconsistentData :: TestData 'ForSpending +-- closeAuctionInconsistentData = SpendingTest dtm redeemer val +-- where +-- dtm = auctionWithBidAuthorDatum + +-- redeemer = +-- NFT.CloseAuctionAct +-- { act'symbol = TestValues.appSymbol +-- } + +-- val = TestValues.oneNft + +-- closeAuctionInconsistentContext :: ContextBuilder 'ForSpending +-- closeAuctionInconsistentContext = +-- paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft auctionCloseInconsistentDatum +-- <> paysSelf mempty auctionCloseInconsistentDatum +-- <> signedWith authorPkh +-- <> includeGovHead + +-- dealingValidator :: Ledger.Validator +-- dealingValidator = error () +-- Ledger.mkValidatorScript $ +-- $$(PlutusTx.compile [||wrap||]) +-- `PlutusTx.applyCode` ($$(PlutusTx.compile [||NFT.mkTxPolicy||]) `PlutusTx.applyCode` PlutusTx.liftCode uniqueAsset) +-- where +-- wrap :: +-- (NFT.DatumNft -> NFT.UserAct -> Ledger.ScriptContext -> Bool) -> +-- (BuiltinData -> BuiltinData -> BuiltinData -> ()) +-- wrap = toTestValidator diff --git a/mlabs/test/Test/NFT/Script/Dealing.hs b/mlabs/test/Test/NFT/Script/Dealing.hs new file mode 100644 index 000000000..d59964e2d --- /dev/null +++ b/mlabs/test/Test/NFT/Script/Dealing.hs @@ -0,0 +1,288 @@ +module Test.NFT.Script.Dealing ( + testDealing, +) where + +import PlutusTx qualified +import PlutusTx.Prelude hiding ((<>)) + +import Data.Semigroup ((<>)) + +import Ledger (unPaymentPubKeyHash) +import Ledger.Typed.Scripts.Validators ( + DatumType, + RedeemerType, + TypedValidator, + ValidatorTypes, + mkTypedValidator, + ) +import PlutusTx.Ratio qualified as R +import Test.NFT.Script.Values qualified as TestValues +import Test.Tasty (TestTree) +import Test.Tasty.Plutus.Context ( + ContextBuilder, + Purpose (ForSpending), + paysToOther, + paysToWallet, + signedWith, + ) +import Test.Tasty.Plutus.Script.Unit ( + shouldValidate, + shouldn'tValidate, + ) +import Test.Tasty.Plutus.TestData (TestData (SpendingTest)) +import Test.Tasty.Plutus.TestScript (TestScript, mkTestValidator, toTestValidator) +import Test.Tasty.Plutus.WithScript (WithScript, withTestScript) + +import Mlabs.NFT.Spooky (toSpooky) +import Mlabs.NFT.Types qualified as NFT +import Mlabs.NFT.Validation qualified as NFT + +testDealing :: TestTree +testDealing = withTestScript "Test NFT dealing validator" dealingValidator $ do + shouldValidate "Can buy from author" validBuyData validBuyContext + shouldValidate "Author can set price when owner" validSetPriceData validSetPriceContext + shouldValidate "Owner can set price" ownerUserOneSetPriceData ownerUserOneSetPriceContext + shouldn'tValidate "Author can't set price when not owner" ownerUserOneSetPriceData authorNotOwnerSetPriceContext + shouldn'tValidate "Can't set price if mismatching id" validSetPriceData mismathingIdSetPriceContext + shouldn'tValidate "Can't buy if not for sale" notForSaleData notForSaleContext + shouldn'tValidate "Can't buy if bid not high enough" bidNotHighEnoughData bidNotHighEnoughContext + shouldn'tValidate "Can't buy if author not paid" validBuyData authorNotPaidContext + shouldn'tValidate "Can't buy if owner not paid" ownerNotPaidData ownerNotPaidContext + shouldn'tValidate "Can't buy if mismatching id" validBuyData mismathingIdBuyContext + +-- TODO: bring back this test if `tasty-plutus` would allow to change datum order +-- shouldn'tValidate "Can't buy with inconsistent datum" validBuyData inconsistentDatumContext + +initialNode :: NFT.NftListNode +initialNode = + NFT.NftListNode + { node'information' = + toSpooky $ + NFT.InformationNft + { info'id' = toSpooky TestValues.testNftId + , info'share' = toSpooky (R.reduce 1 2) + , info'author' = toSpooky . NFT.UserId . toSpooky $ TestValues.authorPkh + , info'owner' = toSpooky . NFT.UserId . toSpooky $ TestValues.authorPkh + , info'price' = toSpooky @(Maybe Integer) $ Just (100 * 1_000_000) + , info'auctionState' = toSpooky @(Maybe NFT.AuctionState) Nothing + } + , node'next' = toSpooky @(Maybe NFT.Pointer) Nothing + , node'appInstance' = toSpooky TestValues.appInstance + } + +initialAuthorDatum :: NFT.DatumNft +initialAuthorDatum = NFT.NodeDatum initialNode + +ownerUserOneDatum :: NFT.DatumNft +ownerUserOneDatum = + NFT.NodeDatum $ + initialNode + { NFT.node'information' = + toSpooky $ + (NFT.node'information initialNode) + { NFT.info'owner' = toSpooky . NFT.UserId . toSpooky $ TestValues.userOnePkh + } + } + +notForSaleDatum :: NFT.DatumNft +notForSaleDatum = + NFT.NodeDatum $ + initialNode + { NFT.node'information' = + toSpooky $ + (NFT.node'information initialNode) + { NFT.info'price' = toSpooky @(Maybe Integer) Nothing + } + } + +ownerNotPaidDatum :: NFT.DatumNft +ownerNotPaidDatum = ownerUserOneDatum + +inconsistentDatum :: NFT.DatumNft +inconsistentDatum = + NFT.NodeDatum $ + initialNode + { NFT.node'information' = + toSpooky $ + (NFT.node'information initialNode) + { NFT.info'share' = toSpooky (R.reduce 1 10) + } + } + +-- Buy test cases + +validBuyData :: TestData ( 'ForSpending NFT.DatumNft NFT.UserAct) +validBuyData = SpendingTest dtm redeemer val + where + dtm = initialAuthorDatum + + redeemer = + NFT.BuyAct + { act'bid' = toSpooky @Integer (100 * 1_000_000) + , act'newPrice' = toSpooky @(Maybe Integer) Nothing + , act'symbol' = toSpooky TestValues.appSymbol + } + val = TestValues.adaValue 100 <> TestValues.oneNft + +notForSaleData :: TestData ( 'ForSpending NFT.DatumNft NFT.UserAct) +notForSaleData = SpendingTest dtm redeemer val + where + dtm = notForSaleDatum + + redeemer = + NFT.BuyAct + { act'bid' = toSpooky @Integer (100 * 1_000_000) + , act'newPrice' = toSpooky @(Maybe Integer) $ Just 150 + , act'symbol' = toSpooky TestValues.appSymbol + } + val = TestValues.adaValue 100 <> TestValues.oneNft + +bidNotHighEnoughData :: TestData ( 'ForSpending NFT.DatumNft NFT.UserAct) +bidNotHighEnoughData = SpendingTest dtm redeemer val + where + dtm = initialAuthorDatum + + redeemer = + NFT.BuyAct + { act'bid' = toSpooky @Integer (90 * 1_000_000) + , act'newPrice' = toSpooky @(Maybe Integer) Nothing + , act'symbol' = toSpooky TestValues.appSymbol + } + val = TestValues.adaValue 90 <> TestValues.oneNft + +ownerNotPaidData :: TestData ( 'ForSpending NFT.DatumNft NFT.UserAct) +ownerNotPaidData = SpendingTest dtm redeemer val + where + dtm = ownerNotPaidDatum + + redeemer = + NFT.BuyAct + { act'bid' = toSpooky @Integer (100 * 1_000_000) + , act'newPrice' = toSpooky @(Maybe Integer) Nothing + , act'symbol' = toSpooky TestValues.appSymbol + } + val = TestValues.adaValue 0 <> TestValues.oneNft + +inconsistentDatumData :: TestData ( 'ForSpending NFT.DatumNft NFT.UserAct) +inconsistentDatumData = SpendingTest dtm redeemer val + where + dtm = initialAuthorDatum + + redeemer = + NFT.BuyAct + { act'bid' = toSpooky @Integer (100 * 1_000_000) + , act'newPrice' = toSpooky @(Maybe Integer) Nothing + , act'symbol' = toSpooky TestValues.appSymbol + } + val = TestValues.adaValue 100 <> TestValues.oneNft + +validBuyContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +validBuyContext = + paysToWallet TestValues.authorWallet (TestValues.adaValue 100) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft initialAuthorDatum + <> TestValues.includeGovHead + +bidNotHighEnoughContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +bidNotHighEnoughContext = + paysToWallet TestValues.authorWallet (TestValues.adaValue 90) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft initialAuthorDatum + <> TestValues.includeGovHead + +notForSaleContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +notForSaleContext = + paysToWallet TestValues.userOneWallet TestValues.oneNft + <> paysToWallet TestValues.authorWallet (TestValues.adaValue 100) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft notForSaleDatum + <> TestValues.includeGovHead + +authorNotPaidContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +authorNotPaidContext = + paysToWallet TestValues.authorWallet (TestValues.adaValue 5) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft initialAuthorDatum + <> TestValues.includeGovHead + +ownerNotPaidContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +ownerNotPaidContext = + paysToWallet TestValues.authorWallet (TestValues.adaValue 50) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft ownerNotPaidDatum + <> TestValues.includeGovHead + +inconsistentDatumContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +inconsistentDatumContext = + paysToWallet TestValues.userOneWallet TestValues.oneNft + <> paysToWallet TestValues.authorWallet (TestValues.adaValue 100) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft inconsistentDatum + <> TestValues.includeGovHead + +mismathingIdBuyContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +mismathingIdBuyContext = + paysToWallet TestValues.authorWallet (TestValues.adaValue 100) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft dtm + <> TestValues.includeGovHead + where + dtm = + NFT.NodeDatum $ + initialNode + { NFT.node'information' = toSpooky ((NFT.node'information initialNode) {NFT.info'id' = toSpooky . NFT.NftId . toSpooky @BuiltinByteString $ "I AM INVALID"}) + } + +-- SetPrice test cases + +validSetPriceData :: TestData ( 'ForSpending NFT.DatumNft NFT.UserAct) +validSetPriceData = SpendingTest dtm redeemer val + where + dtm = initialAuthorDatum + + redeemer = + NFT.SetPriceAct + { act'newPrice' = toSpooky @(Maybe Integer) $ Just (150 * 1_000_000) + , act'symbol' = toSpooky TestValues.appSymbol + } + val = TestValues.oneNft + +ownerUserOneSetPriceData :: TestData ( 'ForSpending NFT.DatumNft NFT.UserAct) +ownerUserOneSetPriceData = SpendingTest dtm redeemer val + where + dtm = ownerUserOneDatum + + redeemer = + NFT.SetPriceAct + { act'newPrice' = toSpooky @(Maybe Integer) Nothing + , act'symbol' = toSpooky TestValues.appSymbol + } + val = TestValues.oneNft + +validSetPriceContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +validSetPriceContext = + signedWith (unPaymentPubKeyHash TestValues.authorPkh) + -- TODO: choose between `paysToOther NFT.txValHash` and `output` (see below) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft initialAuthorDatum + +-- <> (output $ Output (OwnType $ toBuiltinData initialAuthorDatum) TestValues.oneNft) + +ownerUserOneSetPriceContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +ownerUserOneSetPriceContext = + signedWith (unPaymentPubKeyHash TestValues.userOnePkh) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft ownerUserOneDatum + +authorNotOwnerSetPriceContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +authorNotOwnerSetPriceContext = + signedWith (unPaymentPubKeyHash TestValues.authorPkh) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft ownerUserOneDatum + +mismathingIdSetPriceContext :: ContextBuilder ( 'ForSpending NFT.DatumNft NFT.UserAct) +mismathingIdSetPriceContext = + signedWith (unPaymentPubKeyHash TestValues.authorPkh) + <> paysToOther (NFT.txValHash TestValues.uniqueAsset) TestValues.oneNft dtm + where + dtm = + NFT.NodeDatum $ + initialNode + { NFT.node'information' = toSpooky ((NFT.node'information initialNode) {NFT.info'id' = toSpooky . NFT.NftId . toSpooky @BuiltinByteString $ "I AM INVALID"}) + } + +dealingValidator :: TestScript ( 'ForSpending NFT.DatumNft NFT.UserAct) +dealingValidator = + mkTestValidator + ($$(PlutusTx.compile [||NFT.mkTxPolicy||]) `PlutusTx.applyCode` PlutusTx.liftCode TestValues.uniqueAsset) + $$(PlutusTx.compile [||toTestValidator||]) diff --git a/mlabs/test/Test/NFT/Script/Main.hs b/mlabs/test/Test/NFT/Script/Main.hs new file mode 100644 index 000000000..f42c201c2 --- /dev/null +++ b/mlabs/test/Test/NFT/Script/Main.hs @@ -0,0 +1,16 @@ +module Test.NFT.Script.Main where + +import Test.NFT.Script.Auction +import Test.NFT.Script.Dealing +import Test.NFT.Script.Minting +import Test.Tasty (TestTree, testGroup) + +test :: TestTree +test = + testGroup + "Script" + [ testMinting + , testDealing + -- , testAuctionBeforeDeadline + -- , testAuctionAfterDeadline + ] diff --git a/mlabs/test/Test/NFT/Script/Minting.hs b/mlabs/test/Test/NFT/Script/Minting.hs new file mode 100644 index 000000000..d59c0631b --- /dev/null +++ b/mlabs/test/Test/NFT/Script/Minting.hs @@ -0,0 +1,142 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Test.NFT.Script.Minting ( + testMinting, +) where + +import Data.Semigroup ((<>)) +import Ledger (unPaymentPubKeyHash) +import Ledger.Value (AssetClass (..), singleton) +import PlutusTx qualified +import PlutusTx.Positive (positive) +import PlutusTx.Prelude hiding ((<>)) +import PlutusTx.Prelude qualified as PlutusPrelude +import PlutusTx.Ratio qualified as R + +import Test.NFT.Script.Values as TestValues +import Test.Tasty (TestTree, localOption) +import Test.Tasty.Plutus.Context +import Test.Tasty.Plutus.Options (TestCurrencySymbol (TestCurrencySymbol), TestTxId (TestTxId)) +import Test.Tasty.Plutus.Script.Unit +import Test.Tasty.Plutus.TestData (TestData (MintingTest), Tokens (Tokens), mintTokens) +import Test.Tasty.Plutus.TestScript (TestScript, mkTestMintingPolicy, toTestMintingPolicy) +import Test.Tasty.Plutus.WithScript (withTestScript) + +import Mlabs.NFT.Spooky (toSpooky, toSpookyAssetClass, unSpookyTokenName) +import Mlabs.NFT.Types qualified as NFT +import Mlabs.NFT.Validation qualified as NFT + +testMinting :: TestTree +testMinting = localOption (TestCurrencySymbol TestValues.nftCurrencySymbol) $ + localOption (TestTxId TestValues.testTxId) $ + withTestScript "Test NFT minting policy" nftMintPolicy $ do + shouldValidate "Valid case" validData validCtx + shouldn'tValidate "Not minting" validData noMintingCtx + shouldn'tValidate "No payee" validData noPayeeCtx + shouldn'tValidate "Pays wrong amount" validData wrongAmountCtx + shouldn'tValidate "Mismatching id" validData mismatchingIdCtx + +baseCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +baseCtx = + -- FIXME: hacky way to pass "UTXO not consumed" + spendsFromPubKey (unPaymentPubKeyHash TestValues.authorPkh) TestValues.oneAda + +mintingCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +mintingCtx = mintsValue $ singleton TestValues.nftCurrencySymbol (unSpookyTokenName TestValues.testTokenName) 1 + +paysNftToScriptCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +paysNftToScriptCtx = paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft () + +paysDatumToScriptCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +paysDatumToScriptCtx = + spendsFromOther (NFT.txValHash uniqueAsset) TestValues.oneNft (NFT.HeadDatum $ NFT.NftListHead (toSpooky @(Maybe NFT.Pointer) Nothing) (toSpooky TestValues.appInstance)) + <> paysToOther (NFT.txValHash uniqueAsset) mempty nodeDatum + <> paysToOther (NFT.txValHash uniqueAsset) mempty headDatum + where + nodeDatum = + NFT.NodeDatum $ + NFT.NftListNode + { node'information' = + toSpooky $ + NFT.InformationNft + { info'id' = toSpooky TestValues.testNftId + , info'share' = toSpooky (R.reduce 1 2) + , info'author' = toSpooky . NFT.UserId . toSpooky $ TestValues.authorPkh + , info'owner' = toSpooky . NFT.UserId . toSpooky $ TestValues.authorPkh + , info'price' = toSpooky $ Just (100 * 1_000_000 :: Integer) + , info'auctionState' = toSpooky @(Maybe NFT.AuctionState) Nothing + } + , node'next' = toSpooky @(Maybe NFT.Pointer) Nothing + , node'appInstance' = toSpooky TestValues.appInstance + } + ptr = NFT.Pointer . toSpooky . toSpookyAssetClass $ AssetClass (TestValues.nftCurrencySymbol, unSpookyTokenName TestValues.testTokenName) + headDatum = NFT.HeadDatum $ NFT.NftListHead (toSpooky $ Just ptr) (toSpooky TestValues.appInstance) + +paysWrongAmountCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +paysWrongAmountCtx = + baseCtx <> mintingCtx + <> paysToOther + (NFT.txValHash uniqueAsset) + (TestValues.oneNft PlutusPrelude.<> TestValues.oneNft) + TestValues.testNftId + +validCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +validCtx = baseCtx <> mintingCtx <> paysNftToScriptCtx <> paysDatumToScriptCtx + +noMintingCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +noMintingCtx = baseCtx <> paysNftToScriptCtx <> paysDatumToScriptCtx + +noPayeeCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +noPayeeCtx = baseCtx <> paysDatumToScriptCtx <> paysNftToScriptCtx + +validData :: TestData ( 'ForMinting NFT.MintAct) +validData = + MintingTest + (NFT.Mint $ toSpooky TestValues.testNftId) + (mintTokens (Tokens (unSpookyTokenName TestValues.testTokenName) [positive| 1 |])) + +nonMintingCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +nonMintingCtx = + paysToOther (NFT.txValHash uniqueAsset) TestValues.oneNft TestValues.testNftId + <> spendsFromPubKey (unPaymentPubKeyHash TestValues.authorPkh) TestValues.oneAda + +wrongAmountCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +wrongAmountCtx = + baseCtx <> mintingCtx <> paysDatumToScriptCtx + <> paysToOther (NFT.txValHash uniqueAsset) (TestValues.oneNft PlutusPrelude.<> TestValues.oneNft) () + +mismatchingIdCtx :: ContextBuilder ( 'ForMinting NFT.MintAct) +mismatchingIdCtx = + baseCtx + <> mintingCtx + <> paysNftToScriptCtx + <> spendsFromOther (NFT.txValHash uniqueAsset) TestValues.oneNft (NFT.HeadDatum $ NFT.NftListHead (toSpooky @(Maybe NFT.Pointer) Nothing) (toSpooky TestValues.appInstance)) + <> paysToOther (NFT.txValHash uniqueAsset) mempty nodeDatum + <> paysToOther (NFT.txValHash uniqueAsset) mempty headDatum + where + nodeDatum = + NFT.NodeDatum $ + NFT.NftListNode + { node'information' = + toSpooky $ + NFT.InformationNft + { info'id' = toSpooky . NFT.NftId . toSpooky @BuiltinByteString $ "I AM INVALID" + , info'share' = toSpooky (R.reduce 1 2) + , info'author' = toSpooky . NFT.UserId . toSpooky $ TestValues.authorPkh + , info'owner' = toSpooky . NFT.UserId . toSpooky $ TestValues.authorPkh + , info'price' = toSpooky $ Just (100 * 1_000_000 :: Integer) + , info'auctionState' = toSpooky @(Maybe NFT.AuctionState) Nothing + } + , node'next' = toSpooky @(Maybe NFT.Pointer) Nothing + , node'appInstance' = toSpooky TestValues.appInstance + } + ptr = NFT.Pointer . toSpooky . toSpookyAssetClass $ AssetClass (TestValues.nftCurrencySymbol, unSpookyTokenName TestValues.testTokenName) + headDatum = NFT.HeadDatum $ NFT.NftListHead (toSpooky $ Just ptr) (toSpooky TestValues.appInstance) + +nftMintPolicy :: TestScript ( 'ForMinting NFT.MintAct) +nftMintPolicy = + mkTestMintingPolicy + ( $$(PlutusTx.compile [||NFT.mkMintPolicy||]) + `PlutusTx.applyCode` PlutusTx.liftCode TestValues.appInstance + ) + $$(PlutusTx.compile [||toTestMintingPolicy||]) diff --git a/mlabs/test/Test/NFT/Script/Values.hs b/mlabs/test/Test/NFT/Script/Values.hs new file mode 100644 index 000000000..4b38a4a84 --- /dev/null +++ b/mlabs/test/Test/NFT/Script/Values.hs @@ -0,0 +1,124 @@ +module Test.NFT.Script.Values where + +import Data.Aeson qualified as Aeson +import Data.Maybe (fromJust) +import Ledger qualified + +import Ledger.CardanoWallet qualified as CardanoWallet +import Ledger.Value qualified as Value +import Test.Tasty.Plutus.Context + +import Plutus.V1.Ledger.Ada qualified as Ada +import PlutusTx.Prelude hiding ((<>)) +import PlutusTx.Ratio qualified as R +import Wallet.Emulator.Wallet qualified as Emu + +import Mlabs.NFT.Contract.Aux qualified as NFT +import Mlabs.NFT.Contract.Init (uniqueTokenName) +import Mlabs.NFT.Governance +import Mlabs.NFT.Governance qualified as Gov +import Mlabs.NFT.Spooky +import Mlabs.NFT.Types +import Mlabs.NFT.Validation qualified as NFT + +-- test values + +-- NFT Author +authorWallet :: Emu.Wallet +authorWallet = Emu.fromWalletNumber (CardanoWallet.WalletNumber 1) + +authorAddr :: Ledger.Address +authorAddr = Emu.mockWalletAddress authorWallet + +authorPkh :: Ledger.PaymentPubKeyHash +authorPkh = Emu.mockWalletPaymentPubKeyHash authorWallet + +-- User 1 +userOneWallet :: Emu.Wallet +userOneWallet = Emu.fromWalletNumber (CardanoWallet.WalletNumber 2) + +userOnePkh :: Ledger.PaymentPubKeyHash +userOnePkh = Emu.mockWalletPaymentPubKeyHash userOneWallet + +-- User 2 +userTwoWallet :: Emu.Wallet +userTwoWallet = Emu.fromWalletNumber (CardanoWallet.WalletNumber 3) + +userTwoPkh :: Ledger.PaymentPubKeyHash +userTwoPkh = Emu.mockWalletPaymentPubKeyHash userTwoWallet + +-- User 3 +userThreeWallet :: Emu.Wallet +userThreeWallet = Emu.fromWalletNumber (CardanoWallet.WalletNumber 4) + +userThreePkh :: Ledger.PaymentPubKeyHash +userThreePkh = Emu.mockWalletPaymentPubKeyHash userThreeWallet + +testTxId :: Ledger.TxId +testTxId = fromJust $ Aeson.decode "{\"getTxId\" : \"61626364\"}" + +testTokenName :: TokenName +testTokenName = TokenName . toSpooky $ hData + where + hData = NFT.hashData . Content . toSpooky @BuiltinByteString $ "A painting." + +testNftId :: NftId +testNftId = NftId . toSpooky . unTokenName $ testTokenName + +nftPolicy :: Ledger.MintingPolicy +nftPolicy = NFT.mintPolicy appInstance + +oneNft :: Value.Value +oneNft = Value.singleton nftCurrencySymbol (unSpookyTokenName testTokenName) 1 + +nftCurrencySymbol :: Value.CurrencySymbol +nftCurrencySymbol = unSpookyCurrencySymbol . app'symbol $ appSymbol + +oneAda :: Value.Value +oneAda = Ada.lovelaceValueOf 1_000_000 + +adaValue :: Integer -> Value.Value +adaValue = Ada.lovelaceValueOf . (* 1_000_000) + +testStateAddr :: UniqueToken -> Ledger.Address +testStateAddr = unSpookyAddress . NFT.txScrAddress + +appInstance :: NftAppInstance +appInstance = NftAppInstance (toSpooky . toSpookyAddress . testStateAddr $ uniqueAsset) (toSpooky uniqueAsset) (toSpooky . toSpookyAddress $ Gov.govScrAddress uniqueAsset) (toSpooky [UserId . toSpooky $ userOnePkh]) + +appSymbol :: NftAppSymbol +appSymbol = NftAppSymbol . toSpooky . NFT.curSymbol $ appInstance + +{- + We can't get rid of hard-coding the CurrencySymbol of UniqueToken at the moment since the mintContract produces it + which works inside the Contract monad. Getting this value from our initApp endpoint need to encapsulate almost everything here + to a Contract monad or using a similar approach such as ScriptM, which is operationally heavy and isn't worth doing. + We can almost make sure that this value won't change unless upgrading weird things in plutus, or predetermining + the initial state UTxOs to something other than the default. +-} + +-- | Hardcoded UniqueToken +{-# INLINEABLE uniqueAsset #-} +uniqueAsset :: UniqueToken +uniqueAsset = assetClass (CurrencySymbol . toSpooky @BuiltinByteString $ "00a6b45b792d07aa2a778d84c49c6a0d0c0b2bf80d6c1c16accdbe01") (TokenName . toSpooky $ uniqueTokenName) + +includeGovHead :: ContextBuilder a +includeGovHead = paysToOther (NFT.txValHash uniqueAsset) (Value.assetClassValue (unSpookyAssetClass uniqueAsset) 1) govHeadDatum + where + govHeadDatum = GovDatum $ HeadLList (GovLHead (R.reduce 5 1000) "") Nothing + +{-# INLINEABLE reportParseFailed #-} +reportParseFailed :: BuiltinString -> () +reportParseFailed what = report ("Parse failed: " `appendString` what) + +{-# INLINEABLE reportPass #-} +reportPass :: () +reportPass = report "Pass" + +{-# INLINEABLE reportFail #-} +reportFail :: () +reportFail = report "Fail" + +{-# INLINEABLE report #-} +report :: BuiltinString -> () +report what = trace ("tasty-plutus: " `appendString` what) () diff --git a/mlabs/test/Test/NFT/Size.hs b/mlabs/test/Test/NFT/Size.hs new file mode 100644 index 000000000..1049a905f --- /dev/null +++ b/mlabs/test/Test/NFT/Size.hs @@ -0,0 +1,21 @@ +module Test.NFT.Size (test) where + +import Plutus.V1.Ledger.Scripts (Script, fromCompiledCode) +import PlutusTx qualified +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.Plutus.Script.Size (fitsOnChain) +import Prelude (String) + +import Mlabs.NFT.Validation (mkTxPolicy) + +test :: TestTree +test = testGroup "Size" [testFitOnChain] + +testFitOnChain :: TestTree +testFitOnChain = fitsOnChain scriptName script + +scriptName :: String +scriptName = "NFT marketplace" + +script :: Script +script = fromCompiledCode $$(PlutusTx.compile [||mkTxPolicy||]) diff --git a/mlabs/test/Test/NFT/Trace.hs b/mlabs/test/Test/NFT/Trace.hs new file mode 100644 index 000000000..40c27c03e --- /dev/null +++ b/mlabs/test/Test/NFT/Trace.hs @@ -0,0 +1,458 @@ +module Test.NFT.Trace ( + AppInitHandle, + appInitTrace, + mintFail1, + mintTrace, + setPriceTrace, + testAny, + testInit, + testMint, + testMint2, + test, + testAuction1, + severalBuysTest, + testGetContent2, + testGetContent1, + test2Admins, +) where + +import PlutusTx.Prelude +import Prelude qualified as Hask + +import Data.Default (def) +import Data.Monoid (Last (..)) +import Data.Text (Text) + +import Control.Monad (void) +import Control.Monad.Freer.Extras.Log as Extra (logInfo) + +import Ledger.TimeSlot (slotToBeginPOSIXTime) +import Plutus.Trace.Emulator (EmulatorTrace, activateContractWallet, callEndpoint, runEmulatorTraceIO) +import Plutus.Trace.Emulator qualified as Trace +import PlutusTx.Ratio qualified as R +import Wallet.Emulator qualified as Emulator + +import Mlabs.NFT.Api +import Mlabs.NFT.Contract.Aux (hashData) +import Mlabs.NFT.Spooky +import Mlabs.NFT.Types +import Mlabs.Utils.Wallet (walletFromNumber) + +-- | Generic application Trace Handle. +type AppTraceHandle = Trace.ContractHandle UserWriter NFTAppSchema Text + +-- | Initialisation Trace Handle. +type AppInitHandle = Trace.ContractHandle (Last NftAppInstance) NFTAppSchema Text + +-- | Initialise the Application +appInitTrace :: EmulatorTrace NftAppInstance +appInitTrace = do + let admin = walletFromNumber 4 :: Emulator.Wallet + let params = + InitParams + [UserId . toSpooky . Emulator.mockWalletPaymentPubKeyHash $ admin] + (R.reduce 5 1000) + (unPaymentPubKeyHash . toSpookyPaymentPubKeyHash . Emulator.mockWalletPaymentPubKeyHash $ admin) + hAdmin :: AppInitHandle <- activateContractWallet admin adminEndpoints + callEndpoint @"app-init" hAdmin params + void $ Trace.waitNSlots 3 + oState <- Trace.observableState hAdmin + appInstace <- case getLast oState of + Nothing -> Trace.throwError $ Trace.GenericError "App Instance Could not be established." + Just aS -> return aS + void $ Trace.waitNSlots 1 + return appInstace + +mintTrace :: UniqueToken -> Emulator.Wallet -> EmulatorTrace NftId +mintTrace aSymb wallet = do + h1 :: AppTraceHandle <- activateContractWallet wallet $ endpoints aSymb + callEndpoint @"mint" h1 artwork + void $ Trace.waitNSlots 1 + return . NftId . toSpooky . hashData . mp'content $ artwork + where + artwork = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + +-- | Emulator Trace 1. Mints one NFT. +mint1Trace :: EmulatorTrace () +mint1Trace = do + appInstance <- appInitTrace + let uniqueToken = appInstance'UniqueToken appInstance + wallet1 = walletFromNumber 1 :: Emulator.Wallet + h1 :: AppTraceHandle <- activateContractWallet wallet1 $ endpoints uniqueToken + + callEndpoint @"mint" h1 artwork + void $ Trace.waitNSlots 1 + where + artwork = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + +getContentTrace1 :: EmulatorTrace () +getContentTrace1 = do + appInstance <- appInitTrace + let uniqueToken = appInstance'UniqueToken appInstance + let wallet1 = walletFromNumber 1 :: Emulator.Wallet + h1 :: AppTraceHandle <- activateContractWallet wallet1 $ endpoints uniqueToken + + callEndpoint @"mint" h1 artwork + void $ Trace.waitNSlots 1 + + h1' :: AppTraceHandle <- activateContractWallet wallet1 $ queryEndpoints uniqueToken + + callEndpoint @"query-content" h1' $ Content . toSpooky @BuiltinByteString $ "A painting." + void $ Trace.waitNSlots 1 + + oState <- Trace.observableState h1' + void $ Trace.waitNSlots 1 + logInfo $ Hask.show oState + void $ Trace.waitNSlots 1 + where + artwork = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + +-- | Two users mint two different artworks. +getContentTrace2 :: EmulatorTrace () +getContentTrace2 = do + appInstance <- appInitTrace + let uniqueToken = appInstance'UniqueToken appInstance + let wallet1 = walletFromNumber 1 :: Emulator.Wallet + h1 :: AppTraceHandle <- activateContractWallet wallet1 $ endpoints uniqueToken + void $ Trace.waitNSlots 1 + h1' :: AppTraceHandle <- activateContractWallet wallet1 $ queryEndpoints uniqueToken + + callEndpoint @"mint" h1 artwork + void $ Trace.waitNSlots 1 + callEndpoint @"mint" h1 artwork2 + void $ Trace.waitNSlots 1 + callEndpoint @"mint" h1 artwork3 + void $ Trace.waitNSlots 1 + callEndpoint @"query-content" h1' $ Content . toSpooky @BuiltinByteString $ "A painting." + void $ Trace.waitNSlots 1 + oState <- Trace.observableState h1' + void $ Trace.waitNSlots 1 + logInfo $ Hask.show oState + void $ Trace.waitNSlots 1 + where + artwork = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + artwork2 = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "Another painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + artwork3 = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "Another painting2." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + +-- | Two users mint two different artworks. +mintTrace2 :: EmulatorTrace () +mintTrace2 = do + appInstance <- appInitTrace + let uniqueToken = appInstance'UniqueToken appInstance + let wallet1 = walletFromNumber 1 :: Emulator.Wallet + h1 :: AppTraceHandle <- activateContractWallet wallet1 $ endpoints uniqueToken + callEndpoint @"mint" h1 artwork + void $ Trace.waitNSlots 1 + callEndpoint @"mint" h1 artwork2 + void $ Trace.waitNSlots 1 + where + artwork = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + artwork2 = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "Another painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + +findNftId :: forall a b. Last (Either a b) -> Maybe a +findNftId x = case getLast x of + Just (Left x') -> Just x' + _ -> Nothing + +-- | Two users mint the same artwork. Should Fail +mintFail1 :: EmulatorTrace () +mintFail1 = do + appInstance <- appInitTrace + let uniqueToken = appInstance'UniqueToken appInstance + let wallet1 = walletFromNumber 1 :: Emulator.Wallet + h1 :: AppTraceHandle <- activateContractWallet wallet1 $ endpoints uniqueToken + callEndpoint @"mint" h1 artwork + void $ Trace.waitNSlots 1 + callEndpoint @"mint" h1 artwork + where + artwork = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + +-- | Emulator Trace 1. Mints one NFT. +eTrace1 :: EmulatorTrace () +eTrace1 = do + let wallet1 = walletFromNumber 1 :: Emulator.Wallet + wallet2 = walletFromNumber 2 :: Emulator.Wallet + + appInstance <- appInitTrace + let uniqueToken = appInstance'UniqueToken appInstance + + h1 :: AppTraceHandle <- activateContractWallet wallet1 $ endpoints uniqueToken + h2 :: AppTraceHandle <- activateContractWallet wallet2 $ endpoints uniqueToken + callEndpoint @"mint" h1 artwork + -- callEndpoint @"mint" h2 artwork2 + void $ Trace.waitNSlots 1 + oState <- Trace.observableState h1 + nftId <- case findNftId oState of + Nothing -> Trace.throwError (Trace.GenericError "NftId not found") + Just nid -> return nid + void $ Trace.waitNSlots 1 + callEndpoint @"buy" h2 (buyParams nftId) + + logInfo @Hask.String $ Hask.show oState + where + -- callEndpoint @"mint" h1 artwork + artwork = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + buyParams nftId = BuyRequestUser nftId 6 (Just 200) + +severalBuysTrace :: EmulatorTrace () +severalBuysTrace = do + let wallet1 = walletFromNumber 1 :: Emulator.Wallet + wallet2 = walletFromNumber 2 :: Emulator.Wallet + wallet3 = walletFromNumber 3 :: Emulator.Wallet + + appInstance <- appInitTrace + let uniqueToken = appInstance'UniqueToken appInstance + + h1 :: AppTraceHandle <- activateContractWallet wallet1 $ endpoints uniqueToken + h2 :: AppTraceHandle <- activateContractWallet wallet2 $ endpoints uniqueToken + h3 :: AppTraceHandle <- activateContractWallet wallet3 $ endpoints uniqueToken + callEndpoint @"mint" h1 artwork + -- callEndpoint @"mint" h2 artwork2 + void $ Trace.waitNSlots 1 + oState <- Trace.observableState h1 + nftId <- case findNftId oState of + Nothing -> Trace.throwError (Trace.GenericError "NftId not found") + Just nid -> return nid + void $ Trace.waitNSlots 1 + callEndpoint @"buy" h2 (buyParams nftId 6) + void $ Trace.waitNSlots 1 + callEndpoint @"buy" h3 (buyParams nftId 200) + void $ Trace.waitNSlots 1 + callEndpoint @"set-price" h2 (SetPriceParams nftId (Just 20)) + where + -- logInfo @Hask.String $ Hask.show oState + + artwork = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + buyParams nftId bid = BuyRequestUser nftId bid (Just 200) + +setPriceTrace :: EmulatorTrace () +setPriceTrace = do + let wallet1 = walletFromNumber 1 :: Emulator.Wallet + wallet2 = walletFromNumber 5 :: Emulator.Wallet + authMintH <- activateContractWallet wallet1 (endpoints $ error ()) + void $ Trace.waitNSlots 2 + oState <- Trace.observableState authMintH + nftId <- case findNftId oState of + Nothing -> Trace.throwError (Trace.GenericError "NftId not found") + Just nid -> return nid + logInfo $ Hask.show nftId + void $ Trace.waitNSlots 1 + authUseH :: AppTraceHandle <- activateContractWallet wallet1 (endpoints $ error ()) + callEndpoint @"set-price" authUseH (SetPriceParams nftId (Just 20)) + void $ Trace.waitNSlots 1 + callEndpoint @"set-price" authUseH (SetPriceParams nftId (Just (-20))) + void $ Trace.waitNSlots 1 + userUseH :: AppTraceHandle <- activateContractWallet wallet2 (endpoints $ error ()) + callEndpoint @"set-price" userUseH (SetPriceParams nftId Nothing) + void $ Trace.waitNSlots 1 + callEndpoint @"set-price" userUseH (SetPriceParams nftId (Just 30)) + void $ Trace.waitNSlots 1 + +-- queryPriceTrace :: EmulatorTrace () +-- queryPriceTrace = do +-- let wallet1 = walletFromNumber 1 :: Emulator.Wallet +-- wallet2 = walletFromNumber 5 :: Emulator.Wallet +-- authMintH :: AppTraceHandle <- activateContractWallet wallet1 endpoints +-- callEndpoint @"mint" authMintH artwork +-- void $ Trace.waitNSlots 2 +-- oState <- Trace.observableState authMintH +-- nftId <- case getLast oState of +-- Nothing -> Trace.throwError (Trace.GenericError "NftId not found") +-- Just nid -> return nid +-- logInfo $ Hask.show nftId +-- void $ Trace.waitNSlots 1 + +-- authUseH <- activateContractWallet wallet1 endpoints +-- callEndpoint @"set-price" authUseH (SetPriceParams nftId (Just 20)) +-- void $ Trace.waitNSlots 2 + +-- queryHandle <- activateContractWallet wallet2 queryEndpoints +-- callEndpoint @"query-current-price" queryHandle nftId +-- -- hangs if this is not called before `observableState` +-- void $ Trace.waitNSlots 1 +-- queryState <- Trace.observableState queryHandle +-- queriedPrice <- case getLast queryState of +-- Nothing -> Trace.throwError (Trace.GenericError "QueryResponse not found") +-- Just resp -> case resp of +-- QueryCurrentOwner _ -> Trace.throwError (Trace.GenericError "wrong query state, got owner instead of price") +-- QueryCurrentPrice price -> return price +-- logInfo $ "Queried price: " <> Hask.show queriedPrice + +-- callEndpoint @"query-current-owner" queryHandle nftId +-- void $ Trace.waitNSlots 1 +-- queryState2 <- Trace.observableState queryHandle +-- queriedOwner <- case getLast queryState2 of +-- Nothing -> Trace.throwError (Trace.GenericError "QueryResponse not found") +-- Just resp -> case resp of +-- QueryCurrentOwner owner -> return owner +-- QueryCurrentPrice _ -> Trace.throwError (Trace.GenericError "wrong query state, got price instead of owner") +-- logInfo $ "Queried owner: " <> Hask.show queriedOwner + +-- void $ Trace.waitNSlots 1 +-- where +-- artwork = +-- MintParams +-- { mp'content = Content "A painting." +-- , mp'title = Title "Fiona Lisa" +-- , mp'share = R.reduce 1 10 +-- , mp'price = Just 100 +-- } + +-- | Test for initialising the App +testInit :: Hask.IO () +testInit = runEmulatorTraceIO $ void appInitTrace + +-- | Test for Minting one token +testMint :: Hask.IO () +testMint = runEmulatorTraceIO mint1Trace + +testMint2 :: Hask.IO () +testMint2 = runEmulatorTraceIO mintTrace2 + +test2Admins :: Hask.IO () +test2Admins = runEmulatorTraceIO mintTrace2 + +testAny :: EmulatorTrace () -> Hask.IO () +testAny = runEmulatorTraceIO + +testGetContent1 :: Hask.IO () +testGetContent1 = runEmulatorTraceIO getContentTrace1 +testGetContent2 :: Hask.IO () +testGetContent2 = runEmulatorTraceIO getContentTrace2 + +auctionTrace1 :: EmulatorTrace () +auctionTrace1 = do + appInstance <- appInitTrace + let uniqueToken = appInstance'UniqueToken appInstance + let wallet1 = walletFromNumber 1 :: Emulator.Wallet + wallet2 = walletFromNumber 2 :: Emulator.Wallet + wallet3 = walletFromNumber 3 :: Emulator.Wallet + h1 :: AppTraceHandle <- activateContractWallet wallet1 $ endpoints uniqueToken + h2 :: AppTraceHandle <- activateContractWallet wallet2 $ endpoints uniqueToken + h3 :: AppTraceHandle <- activateContractWallet wallet3 $ endpoints uniqueToken + callEndpoint @"mint" h1 artwork + + void $ Trace.waitNSlots 1 + oState <- Trace.observableState h1 + nftId <- case findNftId oState of + Nothing -> Trace.throwError (Trace.GenericError "NftId not found") + Just nid -> return nid + + logInfo @Hask.String $ Hask.show oState + void $ Trace.waitNSlots 1 + + callEndpoint @"auction-open" h1 (openParams nftId) + void $ Trace.waitNSlots 1 + + -- callEndpoint @"set-price" h1 (SetPriceParams nftId (Just 20)) + -- void $ Trace.waitNSlots 1 + + callEndpoint @"auction-bid" h2 (bidParams nftId 1111110) + void $ Trace.waitNSlots 1 + + callEndpoint @"auction-bid" h3 (bidParams nftId 77777700) + void $ Trace.waitNSlots 2 + -- void $ Trace.waitNSlots 12 + + callEndpoint @"auction-close" h1 (closeParams nftId) + void $ Trace.waitNSlots 2 + + callEndpoint @"set-price" h3 (SetPriceParams nftId (Just 20)) + void $ Trace.waitNSlots 5 + + logInfo @Hask.String "auction1 test end" + where + artwork = + MintParams + { mp'content = Content . toSpooky @BuiltinByteString $ "A painting." + , mp'title = Title . toSpooky @BuiltinByteString $ "Fiona Lisa" + , mp'share = R.reduce 1 10 + , mp'price = Just 5 + } + + slotTenTime = slotToBeginPOSIXTime def 10 + openParams nftId = AuctionOpenParams nftId slotTenTime 400 + closeParams nftId = AuctionCloseParams nftId + bidParams = AuctionBidParams + +-- | Test for prototyping. +test :: Hask.IO () +test = runEmulatorTraceIO eTrace1 + +severalBuysTest :: Hask.IO () +severalBuysTest = runEmulatorTraceIO severalBuysTrace + +-- testSetPrice :: Hask.IO () +-- testSetPrice = runEmulatorTraceIO setPriceTrace + +-- testQueryPrice :: Hask.IO () +-- testQueryPrice = runEmulatorTraceIO queryPriceTrace + +testAuction1 :: Hask.IO () +testAuction1 = runEmulatorTraceIO auctionTrace1 diff --git a/mlabs/test/Test/NftStateMachine/Contract.hs b/mlabs/test/Test/NftStateMachine/Contract.hs new file mode 100644 index 000000000..581713e63 --- /dev/null +++ b/mlabs/test/Test/NftStateMachine/Contract.hs @@ -0,0 +1,98 @@ +module Test.NftStateMachine.Contract ( + test, +) where + +import PlutusTx.Prelude hiding (foldMap, mconcat, (<>)) +import Prelude (foldMap, mconcat, (<>)) + +import Plutus.Contract.Test (Wallet (..), checkPredicateOptions) +import Test.NftStateMachine.Init (Script, adaCoin, checkOptions, runScript, userAct, w1, w2, w3) +import Test.Tasty (TestTree, testGroup) + +import Mlabs.Emulator.Scene (Scene, checkScene, owns) +import Mlabs.NftStateMachine.Logic.Types (UserAct (..)) + +test :: TestTree +test = + testGroup + "Contract" + [ check "Buy" buyScene buyScript + , check "Buy twice" buyTwiceScene buyTwiceScript + , check "Sets price without ownership" buyScene failToSetPriceScript + , check "Buy locked NFT" noChangesScene failToBuyLockedScript + , check "Buy not enough price" noChangesScene failToBuyNotEnoughPriceScript + ] + where + check msg scene script = checkPredicateOptions checkOptions msg (checkScene scene) (runScript script) + +-------------------------------------------------------------------------------- +-- buy test + +ownsAda :: Wallet -> Integer -> Scene +ownsAda wal amount = wal `owns` [(adaCoin, amount)] + +noChangesScene :: Scene +noChangesScene = foldMap (`ownsAda` 0) [w1, w2, w3] + +-- | 3 users deposit 50 coins to lending app. Each of them uses different coin. +buyScript :: Script +buyScript = do + userAct w1 $ SetPriceAct (Just 100) + userAct w2 $ BuyAct 100 Nothing + userAct w2 $ SetPriceAct (Just 500) + +buyScene :: Scene +buyScene = + mconcat + [ w1 `ownsAda` 110 + , w2 `ownsAda` (-110) + ] + +-- buy twice + +{- | + * User 2 buys from user 1 + * User 3 buys from user 2 +-} +buyTwiceScript :: Script +buyTwiceScript = do + buyScript + userAct w3 $ BuyAct 500 (Just 1000) + +buyTwiceScene :: Scene +buyTwiceScene = buyScene <> buyTwiceChange + where + buyTwiceChange = + mconcat + [ w1 `ownsAda` 50 + , w2 `ownsAda` 500 + , w3 `ownsAda` (-550) + ] + +-------------------------------------------------------------------------------- +-- fail to set price + +{- | User 1 tries to set price after user 2 owned the NFT. + It should fail. +-} +failToSetPriceScript :: Script +failToSetPriceScript = do + buyScript + userAct w1 $ SetPriceAct (Just 200) + +-------------------------------------------------------------------------------- +-- fail to buy locked + +-- | User 2 tries to buy NFT which is locked (no price is set) +failToBuyLockedScript :: Script +failToBuyLockedScript = do + userAct w2 $ BuyAct 1000 Nothing + +-------------------------------------------------------------------------------- +-- fail to buy with not enough money + +-- | User 2 tries to buy open NFT with not enough money +failToBuyNotEnoughPriceScript :: Script +failToBuyNotEnoughPriceScript = do + userAct w1 $ SetPriceAct (Just 100) + userAct w2 $ BuyAct 10 Nothing diff --git a/mlabs/test/Test/NftStateMachine/Init.hs b/mlabs/test/Test/NftStateMachine/Init.hs new file mode 100644 index 000000000..772d11472 --- /dev/null +++ b/mlabs/test/Test/NftStateMachine/Init.hs @@ -0,0 +1,99 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NumericUnderscores #-} + +-- | Init blockchain state for tests +module Test.NftStateMachine.Init ( + Script, + runScript, + checkOptions, + w1, + w2, + w3, + userAct, + adaCoin, + initialDistribution, + toUserId, + nftContent, +) where + +import Prelude + +import Control.Lens ((&), (.~)) +import Control.Monad.Freer (Eff) +import Control.Monad.Freer.Error (Error) +import Control.Monad.Freer.Extras.Log (LogMsg) +import Control.Monad.Reader (ReaderT, ask, lift, runReaderT) +import Data.Map qualified as M +import Plutus.Contract.Test (CheckOptions, Wallet (..), defaultCheckOptions, emulatorConfig, mockWalletPaymentPubKeyHash) +import Plutus.Trace.Effects.Assert (Assert) +import Plutus.Trace.Effects.EmulatedWalletAPI (EmulatedWalletAPI) +import Plutus.Trace.Effects.EmulatorControl (EmulatorControl) +import Plutus.Trace.Effects.RunContract (RunContract, StartContract) +import Plutus.Trace.Effects.Waiting (Waiting) +import Plutus.Trace.Emulator (EmulatorRuntimeError, EmulatorTrace, initialChainState) +import Plutus.V1.Ledger.Ada (adaSymbol, adaToken) +import Plutus.V1.Ledger.Value (Value, singleton) +import PlutusTx.Prelude (BuiltinByteString) +import Test.Utils (next) + +import Mlabs.Emulator.Types (UserId (..), adaCoin) +import Mlabs.NftStateMachine.Contract qualified as N +import Mlabs.NftStateMachine.Contract.Emulator.Client qualified as N +import Mlabs.NftStateMachine.Logic.Types (NftId, UserAct (..)) +import Mlabs.Utils.Wallet (walletFromNumber) +import PlutusTx.Ratio qualified as R + +checkOptions :: CheckOptions +checkOptions = defaultCheckOptions & emulatorConfig . initialChainState .~ Left initialDistribution + +-- | Wallets that are used for testing. +w1, w2, w3 :: Wallet +w1 = walletFromNumber 1 +w2 = walletFromNumber 2 +w3 = walletFromNumber 3 + +toUserId :: Wallet -> UserId +toUserId = UserId . mockWalletPaymentPubKeyHash + +-- | Helper to run the scripts for NFT-contract +type ScriptM a = ReaderT NftId (Eff '[StartContract, RunContract, Assert, Waiting, EmulatorControl, EmulatedWalletAPI, LogMsg String, Error EmulatorRuntimeError]) a + +type Script = ScriptM () + +{- | Script runner. It inits NFT by user 1 and provides nft id to all sequent + endpoint calls. +-} +runScript :: Script -> EmulatorTrace () +runScript script = do + nftId <- + N.callStartNft w1 $ + N.StartParams + { sp'content = nftContent + , sp'share = R.reduce 1 10 + , sp'price = Nothing + } + next + runReaderT script nftId + +-- | User action call. +userAct :: Wallet -> UserAct -> Script +userAct wal act = do + nftId <- ask + lift $ N.callUserAct nftId wal act >> next + +-- | NFT content for testing. +nftContent :: BuiltinByteString +nftContent = "Mona Lisa" + +{- | Initial distribution of wallets for testing. + We have 3 users. All of them get 1000 lovelace at the start. +-} +initialDistribution :: M.Map Wallet Value +initialDistribution = + M.fromList + [ (w1, val 1000_000_000) + , (w2, val 1000_000_000) + , (w3, val 1000_000_000) + ] + where + val x = singleton adaSymbol adaToken x diff --git a/mlabs/test/Test/NftStateMachine/Logic.hs b/mlabs/test/Test/NftStateMachine/Logic.hs new file mode 100644 index 000000000..4df9450a8 --- /dev/null +++ b/mlabs/test/Test/NftStateMachine/Logic.hs @@ -0,0 +1,127 @@ +-- | Tests for logic of state transitions for aave prototype +module Test.NftStateMachine.Logic ( + test, +) where + +import PlutusTx.Prelude + +import Data.Map.Strict qualified as M +import Plutus.V1.Ledger.Crypto (PubKeyHash (..)) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (testCase) + +import Ledger (PaymentPubKeyHash (PaymentPubKeyHash)) +import Mlabs.Emulator.App (checkWallets, noErrors, someErrors) +import Mlabs.Emulator.Blockchain (BchWallet (..)) +import Mlabs.Emulator.Types (UserId (UserId), adaCoin) +import Mlabs.NftStateMachine.Logic.App (Script, buy, defaultAppCfg, runNftApp, setPrice) + +-- | Test suite for a logic of lending application +test :: TestTree +test = + testGroup + "Logic" + [ testCase "Buy" testBuy + , testCase "Buy twice" testBuyTwice + , testCase "Sets price without ownership" testFailToSetPrice + , testCase "Buy locked NFT" testBuyLocked + , testCase "Buy not enough price" testBuyNotEnoughPrice + ] + where + testBuy = testWallets buyWallets buyScript + testFailToSetPrice = testWalletsFail buyWallets failToSetPriceScript + testBuyLocked = testWalletsFail initWallets failToBuyLocked + testBuyNotEnoughPrice = testWalletsFail initWallets failToBuyNotEnoughPrice + testBuyTwice = testWallets buyTwiceWallets buyTwiceScript + + testWallets wals script = do + noErrors app + checkWallets wals app + where + app = runNftApp defaultAppCfg script + + testWalletsFail wals script = do + someErrors app + checkWallets wals app + where + app = runNftApp defaultAppCfg script + +initWallets :: [(UserId, BchWallet)] +initWallets = [(user1, wal), (user2, wal)] + where + wal = BchWallet $ M.fromList [(adaCoin, 1000)] + +---------------------------------------------------------------------- +-- scripts + +-- buy + +-- | Buy script +buyScript :: Script +buyScript = do + setPrice user1 (Just 100) + buy user2 100 Nothing + setPrice user2 (Just 500) + +-- * User 1 sets the price to 100 + +-- * User 2 buys for 100 and becomes owner + +-- * User 1 receives 110 (100 + 10% as author) +buyWallets :: [(UserId, BchWallet)] +buyWallets = [(user1, w1), (user2, w2)] + where + w1 = BchWallet $ M.fromList [(adaCoin, 1110)] + w2 = BchWallet $ M.fromList [(adaCoin, 890)] + +-- buy twice + +{- | + * User 2 buys from user 1 + * User 3 buys from user 2 +-} +buyTwiceScript :: Script +buyTwiceScript = do + buyScript + buy user3 500 (Just 1000) + +buyTwiceWallets :: [(UserId, BchWallet)] +buyTwiceWallets = [(user1, w1), (user2, w2), (user3, w3)] + where + w1 = BchWallet $ M.fromList [(adaCoin, 1160)] -- 1000 + 100 + 10 + 50 + w2 = BchWallet $ M.fromList [(adaCoin, 1390)] -- 1000 - 100 - 10 + 500 + w3 = BchWallet $ M.fromList [(adaCoin, 450)] -- 1000 - 500 - 50 + +-- fail to set price + +{- | User 1 tries to set price after user 2 owned the NFT. + It should fail. +-} +failToSetPriceScript :: Script +failToSetPriceScript = do + buyScript + setPrice user1 (Just 200) + +-- fail to buy locked + +-- | User 2 tries to buy NFT which is locked (no price is set) +failToBuyLocked :: Script +failToBuyLocked = do + buy user2 1000 Nothing + +-- fail to buy with not enough money + +-- | User 2 tries to buy open NFT with not enough money +failToBuyNotEnoughPrice :: Script +failToBuyNotEnoughPrice = do + setPrice user1 (Just 100) + buy user2 10 Nothing + +---------------------------------------------------------------------- +-- constants + +-- users +user1, user2, user3 :: UserId +user1 = UserId $ PaymentPubKeyHash $ PubKeyHash "1" +user2 = UserId $ PaymentPubKeyHash $ PubKeyHash "2" +user3 = UserId $ PaymentPubKeyHash $ PubKeyHash "3" diff --git a/mlabs/test/Test/Utils.hs b/mlabs/test/Test/Utils.hs new file mode 100644 index 000000000..fa067de5a --- /dev/null +++ b/mlabs/test/Test/Utils.hs @@ -0,0 +1,29 @@ +module Test.Utils ( + throwError, + next, + wait, + concatPredicates, +) where + +import PlutusTx.Prelude hiding (fromInteger) +import Prelude (String, fromInteger) + +import Data.Functor (void) +import Data.List (foldl1') +import Plutus.Contract.Test (TracePredicate, (.&&.)) +import Plutus.Trace.Emulator qualified as Trace + +-- | Throws error to emulator trace. +throwError :: String -> Trace.EmulatorTrace a +throwError msg = Trace.throwError (Trace.GenericError $ "Generic Error:" <> msg) + +-- | Wait for one slot. +next :: Trace.EmulatorTrace () +next = void Trace.nextSlot + +-- | Wait given amount of slots. +wait :: Integer -> Trace.EmulatorTrace () +wait = void . Trace.waitNSlots . fromInteger + +concatPredicates :: [TracePredicate] -> TracePredicate +concatPredicates = foldl1' (.&&.) diff --git a/notes/codegen.md b/notes/codegen.md new file mode 100644 index 000000000..e19f989ba --- /dev/null +++ b/notes/codegen.md @@ -0,0 +1,61 @@ +# Playground frontend form generation + +In order to handle arbitrary datatypes and their values, Plutus uses +classes `ToSchema` and `ToArgument` and the `FormArgument` datatype. +`FormArgument` is based on the `Fix` type from `recursion-schemes` which is used +by some of the frontend code. + +Minimal example on the Haskell side (uses `LockArgs` from `GameStateMachine.hs` use case): + +``` +nix-shell shell.nix +cd plutus-use-cases/ +cabal repl +``` + +``` +import Schema +import Ledger.Value as V +import Ledger.Ada as Ada +import Plutus.Contracts.GameStateMachineargs = LockArgs "hello" (Ada.lovelaceValueOf 10000000) +toArgument args + +Fix (FormObjectF [("lockArgsSecret",Fix (FormStringF (Just "hello"))),("lockArgsValue",Fix (FormValueF (Value (Map [(,Map [("",10000000)])]))))]) +``` + +This is the code responsible for generating endpoint argument forms +in Plutus Playground on the PureScript side: + +https://github.com/input-output-hk/plutus/blob/74cb849b6580d937a97aff42636d4ddc6a140ed6/plutus-playground-client/src/Action/View.purs#L88 +https://github.com/input-output-hk/plutus/blob/74cb849b6580d937a97aff42636d4ddc6a140ed6/web-common-plutus/src/Schema/View.purs#L32-L307 + +(the commit is fixed here for convenience) + +The PureScript frontend uses Halogen to generate HTML, here is the example: + +``` +nix-shell shell.nix +cd plutus-playground-client/ +spago repl +``` + +``` +import Schema.View +import Data.BigInteger as BigInteger +import Data.Functor.Foldable (Fix(..)) +import Data.Int as Int +import Data.Json.JsonTuple (JsonTuple(..)) +import Data.Tuple +import Data.Maybe +import Halogen.HTML +import Schema +import Schema.Types +import Plutus.V1.Ledger.Value +import PlutusTx.AssocMap as AssocMap +import Data.Unit + +v = Fix (FormValueF (Value { getValue: AssocMap.fromTuples [ ( Tuple (CurrencySymbol { unCurrencySymbol: "" }) (AssocMap.fromTuples [ Tuple (TokenName { unTokenName: "" }) (BigInteger.fromInt 10000000) ])) ] })) +s = Fix (FormStringF (Just "hello")) +f = Fix (FormObjectF [JsonTuple (Tuple "lockArgsSecret" s), JsonTuple (Tuple "lockArgsValue" v)]) +html = actionArgumentForm 0 (\_ -> unit) f :: HTML Unit Unit +```