Browse Source

add 165 to 721 (#972)

* make _tokenId indexed in Transfer and Approval events

via: https://github.com/ethereum/EIPs/pull/1124/files

* fix: make name() and symbol() external instead of public

* feat: implement ERC721's ERC165

* feat: erc165 tests

* fix: don't use chai-as-promised in direct await

* fix: reorganize to /introspection

* feat: abstract all erc165 tests to a behavior

* feat: disallow registering 0xffffffff
Matt Condon 7 years ago
parent
commit
259b9da3e6

+ 20 - 0
contracts/introspection/ERC165.sol

@@ -0,0 +1,20 @@
+pragma solidity ^0.4.23;
+
+
+/**
+ * @title ERC165
+ * @dev https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md
+ */
+interface ERC165 {
+
+  /**
+   * @notice Query if a contract implements an interface
+   * @param _interfaceId The interface identifier, as specified in ERC-165
+   * @dev Interface identification is specified in ERC-165. This function
+   * @dev  uses less than 30,000 gas.
+   */
+  function supportsInterface(bytes4 _interfaceId)
+    external
+    view
+    returns (bool);
+}

+ 53 - 0
contracts/introspection/SupportsInterfaceWithLookup.sol

@@ -0,0 +1,53 @@
+pragma solidity ^0.4.23;
+
+import "./ERC165.sol";
+
+
+/**
+ * @title SupportsInterfaceWithLookup
+ * @author Matt Condon (@shrugs)
+ * @dev Implements ERC165 using a lookup table.
+ */
+contract SupportsInterfaceWithLookup is ERC165 {
+  bytes4 public constant InterfaceId_ERC165 = 0x01ffc9a7;
+  /**
+   * 0x01ffc9a7 ===
+   *   bytes4(keccak256('supportsInterface(bytes4)'))
+   */
+
+  /**
+   * @dev a mapping of interface id to whether or not it's supported
+   */
+  mapping(bytes4 => bool) internal supportedInterfaces;
+
+  /**
+   * @dev A contract implementing SupportsInterfaceWithLookup
+   * @dev  implement ERC165 itself
+   */
+  constructor()
+    public
+  {
+    _registerInterface(InterfaceId_ERC165);
+  }
+
+  /**
+   * @dev implement supportsInterface(bytes4) using a lookup table
+   */
+  function supportsInterface(bytes4 _interfaceId)
+    external
+    view
+    returns (bool)
+  {
+    return supportedInterfaces[_interfaceId];
+  }
+
+  /**
+   * @dev private method for registering an interface
+   */
+  function _registerInterface(bytes4 _interfaceId)
+    internal
+  {
+    require(_interfaceId != 0xffffffff);
+    supportedInterfaces[_interfaceId] = true;
+  }
+}

+ 12 - 0
contracts/mocks/SupportsInterfaceWithLookupMock.sol

@@ -0,0 +1,12 @@
+pragma solidity ^0.4.23;
+
+import "../introspection/SupportsInterfaceWithLookup.sol";
+
+
+contract SupportsInterfaceWithLookupMock is SupportsInterfaceWithLookup {
+  function registerInterface(bytes4 _interfaceId)
+    public
+  {
+    _registerInterface(_interfaceId);
+  }
+}

+ 32 - 2
contracts/token/ERC721/ERC721.sol

@@ -8,6 +8,21 @@ import "./ERC721Basic.sol";
  * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
  */
 contract ERC721Enumerable is ERC721Basic {
+
+  bytes4 private constant InterfaceId_ERC721Enumerable = 0x780e9d63;
+  /**
+   * 0x780e9d63 ===
+   *   bytes4(keccak256('totalSupply()')) ^
+   *   bytes4(keccak256('tokenOfOwnerByIndex(address,uint256)')) ^
+   *   bytes4(keccak256('tokenByIndex(uint256)'))
+   */
+
+  constructor()
+    public
+  {
+    _registerInterface(InterfaceId_ERC721Enumerable);
+  }
+
   function totalSupply() public view returns (uint256);
   function tokenOfOwnerByIndex(
     address _owner,
@@ -26,8 +41,23 @@ contract ERC721Enumerable is ERC721Basic {
  * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
  */
 contract ERC721Metadata is ERC721Basic {
-  function name() public view returns (string _name);
-  function symbol() public view returns (string _symbol);
+
+  bytes4 private constant InterfaceId_ERC721Metadata = 0x5b5e139f;
+  /**
+   * 0x5b5e139f ===
+   *   bytes4(keccak256('name()')) ^
+   *   bytes4(keccak256('symbol()')) ^
+   *   bytes4(keccak256('tokenURI(uint256)'))
+   */
+
+  constructor()
+    public
+  {
+    _registerInterface(InterfaceId_ERC721Metadata);
+  }
+
+  function name() external view returns (string _name);
+  function symbol() external view returns (string _symbol);
   function tokenURI(uint256 _tokenId) public view returns (string);
 }
 

+ 33 - 3
contracts/token/ERC721/ERC721Basic.sol

@@ -1,20 +1,22 @@
 pragma solidity ^0.4.23;
 
+import "../../introspection/SupportsInterfaceWithLookup.sol";
+
 
 /**
  * @title ERC721 Non-Fungible Token Standard basic interface
  * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
  */
-contract ERC721Basic {
+contract ERC721Basic is SupportsInterfaceWithLookup {
   event Transfer(
     address indexed _from,
     address indexed _to,
-    uint256 _tokenId
+    uint256 indexed _tokenId
   );
   event Approval(
     address indexed _owner,
     address indexed _approved,
-    uint256 _tokenId
+    uint256 indexed _tokenId
   );
   event ApprovalForAll(
     address indexed _owner,
@@ -22,6 +24,34 @@ contract ERC721Basic {
     bool _approved
   );
 
+  bytes4 private constant InterfaceId_ERC721 = 0x80ac58cd;
+  /*
+   * 0x80ac58cd ===
+   *   bytes4(keccak256('balanceOf(address)')) ^
+   *   bytes4(keccak256('ownerOf(uint256)')) ^
+   *   bytes4(keccak256('approve(address,uint256)')) ^
+   *   bytes4(keccak256('getApproved(uint256)')) ^
+   *   bytes4(keccak256('setApprovalForAll(address,bool)')) ^
+   *   bytes4(keccak256('isApprovedForAll(address,address)')) ^
+   *   bytes4(keccak256('transferFrom(address,address,uint256)')) ^
+   *   bytes4(keccak256('safeTransferFrom(address,address,uint256)')) ^
+   *   bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)'))
+   */
+
+  bytes4 private constant InterfaceId_ERC721Exists = 0x4f558e79;
+  /*
+   * 0x4f558e79 ===
+   *   bytes4(keccak256('exists(uint256)'))
+   */
+
+  constructor ()
+    public
+  {
+    // register the supported interfaces to conform to ERC721 via ERC165
+    _registerInterface(InterfaceId_ERC721);
+    _registerInterface(InterfaceId_ERC721Exists);
+  }
+
   function balanceOf(address _owner) public view returns (uint256 _balance);
   function ownerOf(uint256 _tokenId) public view returns (address _owner);
   function exists(uint256 _tokenId) public view returns (bool _exists);

+ 1 - 1
contracts/token/ERC721/ERC721BasicToken.sol

@@ -16,7 +16,7 @@ contract ERC721BasicToken is ERC721Basic {
 
   // Equals to `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`
   // which can be also obtained as `ERC721Receiver(0).onERC721Received.selector`
-  bytes4 constant ERC721_RECEIVED = 0xf0b9e5ba;
+  bytes4 private constant ERC721_RECEIVED = 0xf0b9e5ba;
 
   // Mapping from token ID to owner
   mapping (uint256 => address) internal tokenOwner;

+ 1 - 1
contracts/token/ERC721/ERC721Receiver.sol

@@ -12,7 +12,7 @@ contract ERC721Receiver {
    *  Equals to `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`,
    *  which can be also obtained as `ERC721Receiver(0).onERC721Received.selector`
    */
-  bytes4 constant ERC721_RECEIVED = 0xf0b9e5ba;
+  bytes4 internal constant ERC721_RECEIVED = 0xf0b9e5ba;
 
   /**
    * @notice Handle the receipt of an NFT

+ 2 - 2
contracts/token/ERC721/ERC721Token.sol

@@ -44,7 +44,7 @@ contract ERC721Token is ERC721, ERC721BasicToken {
    * @dev Gets the token name
    * @return string representing the token name
    */
-  function name() public view returns (string) {
+  function name() external view returns (string) {
     return name_;
   }
 
@@ -52,7 +52,7 @@ contract ERC721Token is ERC721, ERC721BasicToken {
    * @dev Gets the token symbol
    * @return string representing the token symbol
    */
-  function symbol() public view returns (string) {
+  function symbol() external view returns (string) {
     return symbol_;
   }
 

+ 712 - 0
package-lock.json

@@ -25,6 +25,33 @@
         "xtend": "~4.0.0"
       }
     },
+    "accepts": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+      "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+      "dev": true,
+      "requires": {
+        "mime-types": "2.1.18",
+        "negotiator": "0.6.1"
+      },
+      "dependencies": {
+        "mime-db": {
+          "version": "1.33.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
+          "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
+          "dev": true
+        },
+        "mime-types": {
+          "version": "2.1.18",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
+          "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
+          "dev": true,
+          "requires": {
+            "mime-db": "1.33.0"
+          }
+        }
+      }
+    },
     "acorn-jsx": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
@@ -163,6 +190,12 @@
       "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=",
       "dev": true
     },
+    "array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
+      "dev": true
+    },
     "array-union": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
@@ -246,6 +279,12 @@
         }
       }
     },
+    "async-limiter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+      "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==",
+      "dev": true
+    },
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1171,6 +1210,50 @@
       "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
       "dev": true
     },
+    "body-parser": {
+      "version": "1.18.3",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
+      "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "content-type": "1.0.4",
+        "debug": "2.6.9",
+        "depd": "1.1.2",
+        "http-errors": "1.6.3",
+        "iconv-lite": "0.4.23",
+        "on-finished": "2.3.0",
+        "qs": "6.5.2",
+        "raw-body": "2.3.3",
+        "type-is": "1.6.16"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.23",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+          "dev": true,
+          "requires": {
+            "safer-buffer": "2.1.2"
+          }
+        },
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+          "dev": true
+        }
+      }
+    },
     "boom": {
       "version": "2.10.1",
       "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
@@ -1260,6 +1343,12 @@
       "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==",
       "dev": true
     },
+    "buffer-to-arraybuffer": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz",
+      "integrity": "sha1-YGSkD6dutDxyOrqe+PbhIW0QURo=",
+      "dev": true
+    },
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
@@ -1272,6 +1361,12 @@
       "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
       "dev": true
     },
+    "bytes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+      "dev": true
+    },
     "cacheable-request": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz",
@@ -1673,12 +1768,36 @@
       "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=",
       "dev": true
     },
+    "content-disposition": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=",
+      "dev": true
+    },
+    "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==",
+      "dev": true
+    },
     "convert-source-map": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz",
       "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=",
       "dev": true
     },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+      "dev": true
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
+      "dev": true
+    },
     "core-js": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.0.tgz",
@@ -1691,6 +1810,16 @@
       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
       "dev": true
     },
+    "cors": {
+      "version": "2.8.4",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz",
+      "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=",
+      "dev": true,
+      "requires": {
+        "object-assign": "4.1.1",
+        "vary": "1.1.2"
+      }
+    },
     "coveralls": {
       "version": "2.13.1",
       "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.1.tgz",
@@ -1942,6 +2071,18 @@
       "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
       "dev": true
     },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+      "dev": true
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
+      "dev": true
+    },
     "detect-conflict": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/detect-conflict/-/detect-conflict-1.0.1.tgz",
@@ -2017,6 +2158,12 @@
       "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==",
       "dev": true
     },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+      "dev": true
+    },
     "ejs": {
       "version": "2.5.8",
       "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.8.tgz",
@@ -2050,6 +2197,12 @@
       "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
       "dev": true
     },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+      "dev": true
+    },
     "encoding": {
       "version": "0.1.12",
       "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
@@ -2134,6 +2287,12 @@
         "is-symbol": "^1.0.1"
       }
     },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+      "dev": true
+    },
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -2545,6 +2704,27 @@
       "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
       "dev": true
     },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+      "dev": true
+    },
+    "eth-lib": {
+      "version": "0.1.27",
+      "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.1.27.tgz",
+      "integrity": "sha512-B8czsfkJYzn2UIEMwjc7Mbj+Cy72V+/OXH/tb44LV8jhrjizQJJ325xMOMyk3+ETa6r6oi0jsUY14+om8mQMWA==",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "elliptic": "6.4.0",
+        "keccakjs": "0.2.1",
+        "nano-json-stream-parser": "0.1.2",
+        "servify": "0.1.12",
+        "ws": "3.3.3",
+        "xhr-request-promise": "0.1.2"
+      }
+    },
     "ethereum-common": {
       "version": "0.0.16",
       "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.16.tgz",
@@ -2740,6 +2920,24 @@
         }
       }
     },
+    "ethjs-unit": {
+      "version": "0.1.6",
+      "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz",
+      "integrity": "sha1-xmWSHkduh7ziqdWIpv4EBbLEFpk=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.6",
+        "number-to-bn": "1.7.0"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.6",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz",
+          "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=",
+          "dev": true
+        }
+      }
+    },
     "ethjs-util": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.4.tgz",
@@ -2813,6 +3011,129 @@
         "homedir-polyfill": "^1.0.1"
       }
     },
+    "express": {
+      "version": "4.16.3",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
+      "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
+      "dev": true,
+      "requires": {
+        "accepts": "1.3.5",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.18.2",
+        "content-disposition": "0.5.2",
+        "content-type": "1.0.4",
+        "cookie": "0.3.1",
+        "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.1",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "1.1.2",
+        "on-finished": "2.3.0",
+        "parseurl": "1.3.2",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "2.0.3",
+        "qs": "6.5.1",
+        "range-parser": "1.2.0",
+        "safe-buffer": "5.1.1",
+        "send": "0.16.2",
+        "serve-static": "1.13.2",
+        "setprototypeof": "1.1.0",
+        "statuses": "1.4.0",
+        "type-is": "1.6.16",
+        "utils-merge": "1.0.1",
+        "vary": "1.1.2"
+      },
+      "dependencies": {
+        "body-parser": {
+          "version": "1.18.2",
+          "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
+          "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
+          "dev": true,
+          "requires": {
+            "bytes": "3.0.0",
+            "content-type": "1.0.4",
+            "debug": "2.6.9",
+            "depd": "1.1.2",
+            "http-errors": "1.6.3",
+            "iconv-lite": "0.4.19",
+            "on-finished": "2.3.0",
+            "qs": "6.5.1",
+            "raw-body": "2.3.2",
+            "type-is": "1.6.16"
+          }
+        },
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.19",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
+          "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==",
+          "dev": true
+        },
+        "qs": {
+          "version": "6.5.1",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
+          "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==",
+          "dev": true
+        },
+        "raw-body": {
+          "version": "2.3.2",
+          "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
+          "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
+          "dev": true,
+          "requires": {
+            "bytes": "3.0.0",
+            "http-errors": "1.6.2",
+            "iconv-lite": "0.4.19",
+            "unpipe": "1.0.0"
+          },
+          "dependencies": {
+            "depd": {
+              "version": "1.1.1",
+              "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
+              "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=",
+              "dev": true
+            },
+            "http-errors": {
+              "version": "1.6.2",
+              "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
+              "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+              "dev": true,
+              "requires": {
+                "depd": "1.1.1",
+                "inherits": "2.0.3",
+                "setprototypeof": "1.0.3",
+                "statuses": "1.4.0"
+              }
+            },
+            "setprototypeof": {
+              "version": "1.0.3",
+              "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
+              "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=",
+              "dev": true
+            }
+          }
+        },
+        "statuses": {
+          "version": "1.4.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+          "dev": true
+        }
+      }
+    },
     "extend": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@@ -2910,6 +3231,38 @@
         "repeat-string": "^1.5.2"
       }
     },
+    "finalhandler": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+      "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "1.0.2",
+        "escape-html": "1.0.3",
+        "on-finished": "2.3.0",
+        "parseurl": "1.3.2",
+        "statuses": "1.4.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==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "statuses": {
+          "version": "1.4.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+          "dev": true
+        }
+      }
+    },
     "find-up": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
@@ -2994,6 +3347,18 @@
         "mime-types": "^2.1.12"
       }
     },
+    "forwarded": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
+      "dev": true
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+      "dev": true
+    },
     "from2": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
@@ -4098,6 +4463,18 @@
       "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==",
       "dev": true
     },
+    "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=",
+      "dev": true,
+      "requires": {
+        "depd": "1.1.2",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.1.0",
+        "statuses": "1.5.0"
+      }
+    },
     "http-signature": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
@@ -4315,6 +4692,12 @@
       "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=",
       "dev": true
     },
+    "ipaddr.js": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
+      "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=",
+      "dev": true
+    },
     "is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -5485,6 +5868,12 @@
         }
       }
     },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+      "dev": true
+    },
     "mem": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
@@ -5604,6 +5993,12 @@
       "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=",
       "dev": true
     },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
+      "dev": true
+    },
     "merkle-patricia-tree": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-2.1.2.tgz",
@@ -5635,6 +6030,12 @@
         }
       }
     },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+      "dev": true
+    },
     "micromatch": {
       "version": "2.3.11",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
@@ -5656,6 +6057,12 @@
         "regex-cache": "^0.4.2"
       }
     },
+    "mime": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+      "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
+      "dev": true
+    },
     "mime-db": {
       "version": "1.29.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz",
@@ -5831,12 +6238,24 @@
       "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
       "dev": true
     },
+    "nano-json-stream-parser": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz",
+      "integrity": "sha1-DMj20OK2IrR5xA1JnEbWS3Vcb18=",
+      "dev": true
+    },
     "natural-compare": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
       "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
       "dev": true
     },
+    "negotiator": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
+      "dev": true
+    },
     "neo-async": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.0.tgz",
@@ -6032,6 +6451,15 @@
         "is-extendable": "^0.1.1"
       }
     },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "dev": true,
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
     "once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -6254,6 +6682,12 @@
       "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
       "dev": true
     },
+    "parseurl": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
+      "dev": true
+    },
     "path-exists": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
@@ -6287,6 +6721,12 @@
       "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
       "dev": true
     },
+    "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=",
+      "dev": true
+    },
     "path-type": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
@@ -6452,6 +6892,16 @@
       "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=",
       "dev": true
     },
+    "proxy-addr": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
+      "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
+      "dev": true,
+      "requires": {
+        "forwarded": "0.1.2",
+        "ipaddr.js": "1.6.0"
+      }
+    },
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -6547,6 +6997,41 @@
         "safe-buffer": "^5.1.0"
       }
     },
+    "randomhex": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/randomhex/-/randomhex-0.1.5.tgz",
+      "integrity": "sha1-us7vmCMpCRQA8qKRLGzQLxCU9YU=",
+      "dev": true
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
+      "dev": true
+    },
+    "raw-body": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
+      "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "http-errors": "1.6.3",
+        "iconv-lite": "0.4.23",
+        "unpipe": "1.0.0"
+      },
+      "dependencies": {
+        "iconv-lite": {
+          "version": "0.4.23",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+          "dev": true,
+          "requires": {
+            "safer-buffer": "2.1.2"
+          }
+        }
+      }
+    },
     "rc": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz",
@@ -6992,6 +7477,12 @@
       "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
       "dev": true
     },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true
+    },
     "scoped-regex": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/scoped-regex/-/scoped-regex-1.0.0.tgz",
@@ -7055,6 +7546,69 @@
       "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
       "dev": true
     },
+    "send": {
+      "version": "0.16.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+      "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+      "dev": true,
+      "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.6.3",
+        "mime": "1.4.1",
+        "ms": "2.0.0",
+        "on-finished": "2.3.0",
+        "range-parser": "1.2.0",
+        "statuses": "1.4.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "statuses": {
+          "version": "1.4.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+          "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
+          "dev": true
+        }
+      }
+    },
+    "serve-static": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+      "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+      "dev": true,
+      "requires": {
+        "encodeurl": "1.0.2",
+        "escape-html": "1.0.3",
+        "parseurl": "1.3.2",
+        "send": "0.16.2"
+      }
+    },
+    "servify": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/servify/-/servify-0.1.12.tgz",
+      "integrity": "sha512-/xE6GvsKKqyo1BAY+KxOWXcLpPsUUyji7Qg3bVD7hh1eRze5bR1uYiuDA/k3Gof1s9BTzQZEJK8sNcNGFIzeWw==",
+      "dev": true,
+      "requires": {
+        "body-parser": "1.18.3",
+        "cors": "2.8.4",
+        "express": "4.16.3",
+        "request": "2.79.0",
+        "xhr": "2.4.0"
+      }
+    },
     "set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -7067,6 +7621,12 @@
       "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
       "dev": true
     },
+    "setprototypeof": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+      "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+      "dev": true
+    },
     "sha.js": {
       "version": "2.4.8",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz",
@@ -7123,6 +7683,12 @@
       "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
       "dev": true
     },
+    "simple-concat": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
+      "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=",
+      "dev": true
+    },
     "simple-get": {
       "version": "1.4.3",
       "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-1.4.3.tgz",
@@ -7549,6 +8115,12 @@
         }
       }
     },
+    "statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+      "dev": true
+    },
     "stream-to-observable": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.2.0.tgz",
@@ -8197,6 +8769,33 @@
       "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=",
       "dev": true
     },
+    "type-is": {
+      "version": "1.6.16",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+      "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+      "dev": true,
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "2.1.18"
+      },
+      "dependencies": {
+        "mime-db": {
+          "version": "1.33.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
+          "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
+          "dev": true
+        },
+        "mime-types": {
+          "version": "2.1.18",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
+          "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
+          "dev": true,
+          "requires": {
+            "mime-db": "1.33.0"
+          }
+        }
+      }
+    },
     "typedarray": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@@ -8222,6 +8821,12 @@
       "dev": true,
       "optional": true
     },
+    "ultron": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+      "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
+      "dev": true
+    },
     "underscore": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
@@ -8234,6 +8839,12 @@
       "integrity": "sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA=",
       "dev": true
     },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+      "dev": true
+    },
     "untildify": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.2.tgz",
@@ -8255,6 +8866,12 @@
         "prepend-http": "^2.0.0"
       }
     },
+    "url-set-query": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/url-set-query/-/url-set-query-1.0.0.tgz",
+      "integrity": "sha1-AW6M/Xwg7gXK/neV6JK9BwL6ozk=",
+      "dev": true
+    },
     "url-to-options": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
@@ -8273,6 +8890,12 @@
       "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
       "dev": true
     },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+      "dev": true
+    },
     "uuid": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
@@ -8295,6 +8918,12 @@
         "spdx-expression-parse": "~1.0.0"
       }
     },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+      "dev": true
+    },
     "verror": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@@ -8360,6 +8989,41 @@
         "xmlhttprequest": "*"
       }
     },
+    "web3-utils": {
+      "version": "1.0.0-beta.34",
+      "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.0.0-beta.34.tgz",
+      "integrity": "sha1-lBH8OarvOcpOBhafdiKX2f8CCXA=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.6",
+        "eth-lib": "0.1.27",
+        "ethjs-unit": "0.1.6",
+        "number-to-bn": "1.7.0",
+        "randomhex": "0.1.5",
+        "underscore": "1.8.3",
+        "utf8": "2.1.1"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.6",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz",
+          "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=",
+          "dev": true
+        },
+        "underscore": {
+          "version": "1.8.3",
+          "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
+          "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=",
+          "dev": true
+        },
+        "utf8": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.1.tgz",
+          "integrity": "sha1-LgHbAvfY0JRPdxBPFgnrDDBM92g=",
+          "dev": true
+        }
+      }
+    },
     "webpack-addons": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/webpack-addons/-/webpack-addons-1.1.5.tgz",
@@ -8752,6 +9416,17 @@
         "slide": "^1.1.5"
       }
     },
+    "ws": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+      "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+      "dev": true,
+      "requires": {
+        "async-limiter": "1.0.0",
+        "safe-buffer": "5.1.1",
+        "ultron": "1.1.1"
+      }
+    },
     "xhr": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.4.0.tgz",
@@ -8764,6 +9439,43 @@
         "xtend": "^4.0.0"
       }
     },
+    "xhr-request": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/xhr-request/-/xhr-request-1.1.0.tgz",
+      "integrity": "sha512-Y7qzEaR3FDtL3fP30k9wO/e+FBnBByZeybKOhASsGP30NIkRAAkKD/sCnLvgEfAIEC1rcmK7YG8f4oEnIrrWzA==",
+      "dev": true,
+      "requires": {
+        "buffer-to-arraybuffer": "0.0.5",
+        "object-assign": "4.1.1",
+        "query-string": "5.1.1",
+        "simple-get": "2.8.1",
+        "timed-out": "4.0.1",
+        "url-set-query": "1.0.0",
+        "xhr": "2.4.0"
+      },
+      "dependencies": {
+        "simple-get": {
+          "version": "2.8.1",
+          "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz",
+          "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==",
+          "dev": true,
+          "requires": {
+            "decompress-response": "3.3.0",
+            "once": "1.4.0",
+            "simple-concat": "1.0.0"
+          }
+        }
+      }
+    },
+    "xhr-request-promise": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/xhr-request-promise/-/xhr-request-promise-0.1.2.tgz",
+      "integrity": "sha1-NDxE0e53JrhkgGloLQ+EDIO0Jh0=",
+      "dev": true,
+      "requires": {
+        "xhr-request": "1.1.0"
+      }
+    },
     "xhr2": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz",

+ 2 - 1
package.json

@@ -59,6 +59,7 @@
     "solidity-coverage": "^0.5.0",
     "solium": "^1.1.7",
     "truffle": "^4.1.8",
-    "truffle-hdwallet-provider": "0.0.3"
+    "truffle-hdwallet-provider": "0.0.3",
+    "web3-utils": "^1.0.0-beta.34"
   }
 }

+ 21 - 0
test/helpers/makeInterfaceId.js

@@ -0,0 +1,21 @@
+import { soliditySha3 } from 'web3-utils';
+
+const INTERFACE_ID_LENGTH = 4;
+
+export default (interfaces = []) => {
+  const interfaceIdBuffer = interfaces
+    .map(methodSignature => soliditySha3(methodSignature)) // keccak256
+    .map(h =>
+      Buffer
+        .from(h.substring(2), 'hex')
+        .slice(0, 4) // bytes4()
+    )
+    .reduce((memo, bytes) => {
+      for (let i = 0; i < INTERFACE_ID_LENGTH; i++) {
+        memo[i] = memo[i] ^ bytes[i]; // xor
+      }
+      return memo;
+    }, Buffer.alloc(INTERFACE_ID_LENGTH));
+
+  return `0x${interfaceIdBuffer.toString('hex')}`;
+};

+ 54 - 0
test/introspection/SupportsInterface.behavior.js

@@ -0,0 +1,54 @@
+import makeInterfaceId from '../helpers/makeInterfaceId';
+
+const INTERFACE_IDS = {
+  ERC165: makeInterfaceId([
+    'supportsInterface(bytes4)',
+  ]),
+  ERC721: makeInterfaceId([
+    'balanceOf(address)',
+    'ownerOf(uint256)',
+    'approve(address,uint256)',
+    'getApproved(uint256)',
+    'setApprovalForAll(address,bool)',
+    'isApprovedForAll(address,address)',
+    'transferFrom(address,address,uint256)',
+    'safeTransferFrom(address,address,uint256)',
+    'safeTransferFrom(address,address,uint256,bytes)',
+  ]),
+  ERC721Enumerable: makeInterfaceId([
+    'totalSupply()',
+    'tokenOfOwnerByIndex(address,uint256)',
+    'tokenByIndex(uint256)',
+  ]),
+  ERC721Metadata: makeInterfaceId([
+    'name()',
+    'symbol()',
+    'tokenURI(uint256)',
+  ]),
+  ERC721Exists: makeInterfaceId([
+    'exists(uint256)',
+  ]),
+};
+
+export default function (interfaces = []) {
+  describe('ERC165\'s supportsInterface(bytes4)', function () {
+    beforeEach(function () {
+      this.thing = this.mock || this.token;
+    });
+
+    for (let k of interfaces) {
+      const interfaceId = INTERFACE_IDS[k];
+      describe(k, function () {
+        it('should use less than 30k gas', async function () {
+          const gasEstimate = await this.thing.supportsInterface.estimateGas(interfaceId);
+          gasEstimate.should.be.lte(30000);
+        });
+
+        it('is supported', async function () {
+          const isSupported = await this.thing.supportsInterface(interfaceId);
+          isSupported.should.eq(true);
+        });
+      });
+    }
+  });
+}

+ 24 - 0
test/introspection/SupportsInterfaceWithLookup.test.js

@@ -0,0 +1,24 @@
+import shouldSupportInterfaces from './SupportsInterface.behavior';
+import assertRevert from '../helpers/assertRevert';
+
+const SupportsInterfaceWithLookup = artifacts.require('SupportsInterfaceWithLookupMock');
+
+require('chai')
+  .use(require('chai-as-promised'))
+  .should();
+
+contract('SupportsInterfaceWithLookup', function (accounts) {
+  before(async function () {
+    this.mock = await SupportsInterfaceWithLookup.new();
+  });
+
+  it('does not allow 0xffffffff', async function () {
+    await assertRevert(
+      this.mock.registerInterface(0xffffffff)
+    );
+  });
+
+  shouldSupportInterfaces([
+    'ERC165',
+  ]);
+});

+ 56 - 49
test/token/ERC721/ERC721BasicToken.behaviour.js

@@ -1,3 +1,4 @@
+import shouldSupportInterfaces from '../../introspection/SupportsInterface.behavior';
 import assertRevert from '../../helpers/assertRevert';
 import decodeLogs from '../../helpers/decodeLogs';
 import sendTransaction from '../../helpers/sendTransaction';
@@ -19,28 +20,28 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
   const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
   const RECEIVER_MAGIC_VALUE = '0xf0b9e5ba';
 
-  describe('like a ERC721BasicToken', function () {
+  describe('like an ERC721BasicToken', function () {
     beforeEach(async function () {
       await this.token.mint(creator, firstTokenId, { from: creator });
       await this.token.mint(creator, secondTokenId, { from: creator });
     });
 
     describe('balanceOf', function () {
-      describe('when the given address owns some tokens', function () {
+      context('when the given address owns some tokens', function () {
         it('returns the amount of tokens owned by the given address', async function () {
           const balance = await this.token.balanceOf(creator);
           balance.should.be.bignumber.equal(2);
         });
       });
 
-      describe('when the given address does not own any tokens', function () {
+      context('when the given address does not own any tokens', function () {
         it('returns 0', async function () {
           const balance = await this.token.balanceOf(accounts[1]);
           balance.should.be.bignumber.equal(0);
         });
       });
 
-      describe('when querying the zero address', function () {
+      context('when querying the zero address', function () {
         it('throws', async function () {
           await assertRevert(this.token.balanceOf(0));
         });
@@ -48,7 +49,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
     });
 
     describe('exists', function () {
-      describe('when the token exists', function () {
+      context('when the token exists', function () {
         const tokenId = firstTokenId;
 
         it('should return true', async function () {
@@ -57,7 +58,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
         });
       });
 
-      describe('when the token does not exist', function () {
+      context('when the token does not exist', function () {
         const tokenId = unknownTokenId;
 
         it('should return false', async function () {
@@ -68,7 +69,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
     });
 
     describe('ownerOf', function () {
-      describe('when the given token ID was tracked by this token', function () {
+      context('when the given token ID was tracked by this token', function () {
         const tokenId = firstTokenId;
 
         it('returns the owner of the given token ID', async function () {
@@ -77,7 +78,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
         });
       });
 
-      describe('when the given token ID was not tracked by this token', function () {
+      context('when the given token ID was not tracked by this token', function () {
         const tokenId = unknownTokenId;
 
         it('reverts', async function () {
@@ -93,7 +94,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
       const unauthorized = accounts[4];
       const tokenId = firstTokenId;
       const data = '0x42';
-      
+
       let logs = null;
 
       beforeEach(async function () {
@@ -120,7 +121,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
             logs[0].args._owner.should.be.equal(owner);
             logs[0].args._approved.should.be.equal(ZERO_ADDRESS);
             logs[0].args._tokenId.should.be.bignumber.equal(tokenId);
-  
+
             logs[1].event.should.be.eq('Transfer');
             logs[1].args._from.should.be.equal(owner);
             logs[1].args._to.should.be.equal(this.to);
@@ -149,35 +150,35 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
 
           const newOwnerToken = await this.token.tokenOfOwnerByIndex(this.to, 0);
           newOwnerToken.toNumber().should.be.equal(tokenId);
-          
+
           const previousOwnerToken = await this.token.tokenOfOwnerByIndex(owner, 0);
           previousOwnerToken.toNumber().should.not.be.equal(tokenId);
         });
       };
 
       const shouldTransferTokensByUsers = function (transferFunction) {
-        describe('when called by the owner', function () {
+        context('when called by the owner', function () {
           beforeEach(async function () {
             ({ logs } = await transferFunction.call(this, owner, this.to, tokenId, { from: owner }));
           });
           transferWasSuccessful({ owner, tokenId, approved });
         });
 
-        describe('when called by the approved individual', function () {
+        context('when called by the approved individual', function () {
           beforeEach(async function () {
             ({ logs } = await transferFunction.call(this, owner, this.to, tokenId, { from: approved }));
           });
           transferWasSuccessful({ owner, tokenId, approved });
         });
 
-        describe('when called by the operator', function () {
+        context('when called by the operator', function () {
           beforeEach(async function () {
             ({ logs } = await transferFunction.call(this, owner, this.to, tokenId, { from: operator }));
           });
           transferWasSuccessful({ owner, tokenId, approved });
         });
 
-        describe('when called by the owner without an approved user', function () {
+        context('when called by the owner without an approved user', function () {
           beforeEach(async function () {
             await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner });
             ({ logs } = await transferFunction.call(this, owner, this.to, tokenId, { from: operator }));
@@ -185,7 +186,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
           transferWasSuccessful({ owner, tokenId, approved: null });
         });
 
-        describe('when sent to the owner', function () {
+        context('when sent to the owner', function () {
           beforeEach(async function () {
             ({ logs } = await transferFunction.call(this, owner, owner, tokenId, { from: owner }));
           });
@@ -194,56 +195,56 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
             const newOwner = await this.token.ownerOf(tokenId);
             newOwner.should.be.equal(owner);
           });
-  
+
           it('clears the approval for the token ID', async function () {
             const approvedAccount = await this.token.getApproved(tokenId);
             approvedAccount.should.be.equal(ZERO_ADDRESS);
           });
-  
+
           it('emits an approval and transfer events', async function () {
             logs.length.should.be.equal(2);
             logs[0].event.should.be.eq('Approval');
             logs[0].args._owner.should.be.equal(owner);
             logs[0].args._approved.should.be.equal(ZERO_ADDRESS);
             logs[0].args._tokenId.should.be.bignumber.equal(tokenId);
-  
+
             logs[1].event.should.be.eq('Transfer');
             logs[1].args._from.should.be.equal(owner);
             logs[1].args._to.should.be.equal(owner);
             logs[1].args._tokenId.should.be.bignumber.equal(tokenId);
           });
-  
+
           it('keeps the owner balance', async function () {
             const ownerBalance = await this.token.balanceOf(owner);
             ownerBalance.should.be.bignumber.equal(2);
           });
-  
+
           it('keeps same tokens by index', async function () {
             if (!this.token.tokenOfOwnerByIndex) return;
             const tokensListed = await Promise.all(_.range(2).map(i => this.token.tokenOfOwnerByIndex(owner, i)));
             tokensListed.map(t => t.toNumber()).should.have.members([firstTokenId, secondTokenId]);
           });
         });
-        
-        describe('when the address of the previous owner is incorrect', function () {
+
+        context('when the address of the previous owner is incorrect', function () {
           it('reverts', async function () {
             await assertRevert(transferFunction.call(this, unauthorized, this.to, tokenId, { from: owner }));
           });
         });
 
-        describe('when the sender is not authorized for the token id', function () {
+        context('when the sender is not authorized for the token id', function () {
           it('reverts', async function () {
             await assertRevert(transferFunction.call(this, owner, this.to, tokenId, { from: unauthorized }));
           });
         });
 
-        describe('when the given token ID does not exist', function () {
+        context('when the given token ID does not exist', function () {
           it('reverts', async function () {
             await assertRevert(transferFunction.call(this, owner, this.to, unknownTokenId, { from: owner }));
           });
         });
 
-        describe('when the address to transfer the token to is the zero address', function () {
+        context('when the address to transfer the token to is the zero address', function () {
           it('reverts', async function () {
             await assertRevert(transferFunction.call(this, owner, ZERO_ADDRESS, tokenId, { from: owner }));
           });
@@ -275,15 +276,15 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
           describe('to a user account', function () {
             shouldTransferTokensByUsers(transferFun);
           });
-  
+
           describe('to a valid receiver contract', function () {
             beforeEach(async function () {
               this.receiver = await ERC721Receiver.new(RECEIVER_MAGIC_VALUE, false);
               this.to = this.receiver.address;
             });
-  
+
             shouldTransferTokensByUsers(transferFun);
-  
+
             it('should call onERC721Received', async function () {
               const result = await transferFun.call(this, owner, this.to, tokenId, { from: owner });
               result.receipt.logs.length.should.be.equal(3);
@@ -357,9 +358,9 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
           logs[0].args._tokenId.should.be.bignumber.equal(tokenId);
         });
       };
-      
-      describe('when clearing approval', function () {
-        describe('when there was no prior approval', function () {
+
+      context('when clearing approval', function () {
+        context('when there was no prior approval', function () {
           beforeEach(async function () {
             ({ logs } = await this.token.approve(ZERO_ADDRESS, tokenId, { from: sender }));
           });
@@ -370,8 +371,8 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
             logs.length.should.be.equal(0);
           });
         });
-    
-        describe('when there was a prior approval', function () {
+
+        context('when there was a prior approval', function () {
           beforeEach(async function () {
             await this.token.approve(to, tokenId, { from: sender });
             ({ logs } = await this.token.approve(ZERO_ADDRESS, tokenId, { from: sender }));
@@ -382,8 +383,8 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
         });
       });
 
-      describe('when approving a non-zero address', function () {
-        describe('when there was no prior approval', function () {
+      context('when approving a non-zero address', function () {
+        context('when there was no prior approval', function () {
           beforeEach(async function () {
             ({ logs } = await this.token.approve(to, tokenId, { from: sender }));
           });
@@ -392,7 +393,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
           itEmitsApprovalEvent(to);
         });
 
-        describe('when there was a prior approval to the same address', function () {
+        context('when there was a prior approval to the same address', function () {
           beforeEach(async function () {
             await this.token.approve(to, tokenId, { from: sender });
             ({ logs } = await this.token.approve(to, tokenId, { from: sender }));
@@ -402,7 +403,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
           itEmitsApprovalEvent(to);
         });
 
-        describe('when there was a prior approval to a different address', function () {
+        context('when there was a prior approval to a different address', function () {
           beforeEach(async function () {
             await this.token.approve(accounts[2], tokenId, { from: sender });
             ({ logs } = await this.token.approve(to, tokenId, { from: sender }));
@@ -413,26 +414,26 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
         });
       });
 
-      describe('when the address that receives the approval is the owner', function () {
+      context('when the address that receives the approval is the owner', function () {
         it('reverts', async function () {
           await assertRevert(this.token.approve(sender, tokenId, { from: sender }));
         });
       });
-      
-      describe('when the sender does not own the given token ID', function () {
+
+      context('when the sender does not own the given token ID', function () {
         it('reverts', async function () {
           await assertRevert(this.token.approve(to, tokenId, { from: accounts[2] }));
         });
       });
 
-      describe('when the sender is approved for the given token ID', function () {
+      context('when the sender is approved for the given token ID', function () {
         it('reverts', async function () {
           await this.token.approve(accounts[2], tokenId, { from: sender });
           await assertRevert(this.token.approve(to, tokenId, { from: accounts[2] }));
         });
       });
 
-      describe('when the sender is an operator', function () {
+      context('when the sender is an operator', function () {
         const operator = accounts[2];
         beforeEach(async function () {
           await this.token.setApprovalForAll(operator, true, { from: sender });
@@ -443,7 +444,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
         itEmitsApprovalEvent(to);
       });
 
-      describe('when the given token ID does not exist', function () {
+      context('when the given token ID does not exist', function () {
         it('reverts', async function () {
           await assertRevert(this.token.approve(to, unknownTokenId, { from: sender }));
         });
@@ -453,10 +454,10 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
     describe('setApprovalForAll', function () {
       const sender = creator;
 
-      describe('when the operator willing to approve is not the owner', function () {
+      context('when the operator willing to approve is not the owner', function () {
         const operator = accounts[1];
 
-        describe('when there is no operator approval set by the sender', function () {
+        context('when there is no operator approval set by the sender', function () {
           it('approves the operator', async function () {
             await this.token.setApprovalForAll(operator, true, { from: sender });
 
@@ -475,7 +476,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
           });
         });
 
-        describe('when the operator was set as not approved', function () {
+        context('when the operator was set as not approved', function () {
           beforeEach(async function () {
             await this.token.setApprovalForAll(operator, false, { from: sender });
           });
@@ -505,7 +506,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
           });
         });
 
-        describe('when the operator was already approved', function () {
+        context('when the operator was already approved', function () {
           beforeEach(async function () {
             await this.token.setApprovalForAll(operator, true, { from: sender });
           });
@@ -529,7 +530,7 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
         });
       });
 
-      describe('when the operator is the owner', function () {
+      context('when the operator is the owner', function () {
         const operator = creator;
 
         it('reverts', async function () {
@@ -537,5 +538,11 @@ export default function shouldBehaveLikeERC721BasicToken (accounts) {
         });
       });
     });
+
+    shouldSupportInterfaces([
+      'ERC165',
+      'ERC721',
+      'ERC721Exists',
+    ]);
   });
 };

+ 14 - 5
test/token/ERC721/ERC721Token.test.js

@@ -1,6 +1,7 @@
 import assertRevert from '../../helpers/assertRevert';
 import shouldBehaveLikeERC721BasicToken from './ERC721BasicToken.behaviour';
 import shouldMintAndBurnERC721Token from './ERC721MintBurn.behaviour';
+import shouldSupportInterfaces from '../../introspection/SupportsInterface.behavior';
 import _ from 'lodash';
 
 const BigNumber = web3.BigNumber;
@@ -75,7 +76,7 @@ contract('ERC721Token', function (accounts) {
         await assertRevert(this.token.tokenByIndex(0));
       });
     });
-    
+
     describe('metadata', function () {
       const sampleUri = 'mock://mytoken';
 
@@ -122,7 +123,7 @@ contract('ERC721Token', function (accounts) {
     describe('tokenOfOwnerByIndex', function () {
       const owner = creator;
       const another = accounts[1];
-        
+
       describe('when the given index is lower than the amount of tokens owned by the given address', function () {
         it('returns the token ID placed at the given index', async function () {
           const tokenId = await this.token.tokenOfOwnerByIndex(owner, 0);
@@ -178,14 +179,14 @@ contract('ERC721Token', function (accounts) {
           const owner = accounts[0];
           const newTokenId = 300;
           const anotherNewTokenId = 400;
-          
+
           await this.token.burn(tokenId, { from: owner });
           await this.token.mint(owner, newTokenId, { from: owner });
           await this.token.mint(owner, anotherNewTokenId, { from: owner });
-  
+
           const count = await this.token.totalSupply();
           count.toNumber().should.be.equal(3);
-          
+
           const tokensListed = await Promise.all(_.range(3).map(i => this.token.tokenByIndex(i)));
           const expectedTokens = _.filter(
             [firstTokenId, secondTokenId, newTokenId, anotherNewTokenId],
@@ -196,4 +197,12 @@ contract('ERC721Token', function (accounts) {
       });
     });
   });
+
+  shouldSupportInterfaces([
+    'ERC165',
+    'ERC721',
+    'ERC721Exists',
+    'ERC721Enumerable',
+    'ERC721Metadata',
+  ]);
 });