├── .gitignore ├── .s2i └── bin │ ├── assemble │ ├── run │ └── usage ├── README.md ├── build └── contracts │ ├── Address.json │ ├── ERC165.json │ ├── ERC721.json │ ├── ERC721Burnable.json │ ├── ERC721Enumerable.json │ ├── ERC721Full.json │ ├── ERC721Metadata.json │ ├── ExampleApp.json │ ├── IERC165.json │ ├── IERC721.json │ ├── IERC721Enumerable.json │ ├── IERC721Metadata.json │ ├── IERC721Receiver.json │ ├── LimitedMintableNonFungibleToken.json │ ├── Migrations.json │ └── SafeMath.json ├── client ├── README.md ├── app │ ├── controllers │ │ ├── Game.js │ │ └── Index.js │ ├── index.html │ ├── index.js │ ├── scenes │ │ ├── BootScene.js │ │ ├── CrewScene.js │ │ ├── ReceiptScene.js │ │ ├── TransactionScene.js │ │ └── UnitScene.js │ ├── services │ │ ├── AppWalletService.js │ │ ├── StripeService.js │ │ └── TokenService.js │ ├── utils │ │ └── styles.js │ └── views │ │ ├── LoggedIn.js │ │ ├── LoggedOut.js │ │ └── Transfer.js ├── assets │ ├── character-1.png │ ├── character-2.png │ ├── character-3.png │ ├── character-4.png │ ├── character-5.png │ └── screenshot.png ├── package-lock.json ├── package.json ├── public │ ├── callback.html │ └── style.css ├── server.js └── webpack.config.js ├── contracts ├── ExampleApp.sol ├── LimitedMintableNonFungibleToken.sol └── Migrations.sol ├── migrations ├── 1_initial_migration.js ├── 2_initialize_nft.js └── 3_add_minter.js ├── package-lock.json ├── package.json ├── server ├── README.md ├── app │ ├── app.js │ ├── contract.js │ ├── server.js │ └── transaction.js ├── bitski.config.js ├── index.js ├── package-lock.json └── package.json ├── test ├── app.js └── nft.js └── truffle.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | dist/ 4 | -------------------------------------------------------------------------------- /.s2i/bin/assemble: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Prevent running assemble in builders different than official STI image. 4 | # The official nodejs:8-onbuild already run npm install and use different 5 | # application folder. 6 | [ -d "/usr/src/app" ] && exit 0 7 | 8 | set -e 9 | 10 | # FIXME: Linking of global modules is disabled for now as it causes npm failures 11 | # under RHEL7 12 | # Global modules good to have 13 | # npmgl=$(grep "^\s*[^#\s]" ../etc/npm_global_module_list | sort -u) 14 | # Available global modules; only match top-level npm packages 15 | #global_modules=$(npm ls -g 2> /dev/null | perl -ne 'print "$1\n" if /^\S+\s(\S+)\@[\d\.-]+/' | sort -u) 16 | # List all modules in common 17 | #module_list=$(/usr/bin/comm -12 <(echo "${global_modules}") | tr '\n' ' ') 18 | # Link the modules 19 | #npm link $module_list 20 | 21 | safeLogging () { 22 | if [[ $1 =~ http[s]?://.*@.*$ ]]; then 23 | echo $1 | sed 's/^.*@/redacted@/' 24 | else 25 | echo $1 26 | fi 27 | } 28 | 29 | shopt -s dotglob 30 | echo "---> Installing application source ..." 31 | mv /tmp/src/* ./ 32 | 33 | if [ ! -z $HTTP_PROXY ]; then 34 | echo "---> Setting npm http proxy to" $(safeLogging $HTTP_PROXY) 35 | npm config set proxy $HTTP_PROXY 36 | fi 37 | 38 | if [ ! -z $http_proxy ]; then 39 | echo "---> Setting npm http proxy to" $(safeLogging $http_proxy) 40 | npm config set proxy $http_proxy 41 | fi 42 | 43 | if [ ! -z $HTTPS_PROXY ]; then 44 | echo "---> Setting npm https proxy to" $(safeLogging $HTTPS_PROXY) 45 | npm config set https-proxy $HTTPS_PROXY 46 | fi 47 | 48 | if [ ! -z $https_proxy ]; then 49 | echo "---> Setting npm https proxy to" $(safeLogging $https_proxy) 50 | npm config set https-proxy $https_proxy 51 | fi 52 | 53 | # Change the npm registry mirror if provided 54 | if [ -n "$NPM_MIRROR" ]; then 55 | npm config set registry $NPM_MIRROR 56 | fi 57 | 58 | # Set the DEV_MODE to false by default. 59 | if [ -z "$DEV_MODE" ]; then 60 | export DEV_MODE=false 61 | fi 62 | 63 | # If NODE_ENV is not set by the user, then NODE_ENV is determined by whether 64 | # the container is run in development mode. 65 | if [ -z "$NODE_ENV" ]; then 66 | if [ "$DEV_MODE" == true ]; then 67 | export NODE_ENV=development 68 | else 69 | export NODE_ENV=production 70 | fi 71 | fi 72 | 73 | # If NPM_DIRECTORY is set, change the directory before installing and building 74 | if [ ! -z "$NPM_DIRECTORY" ]; then 75 | echo "---> Setting subdirectory to " $(safeLogging $NPM_DIRECTORY) 76 | cd $NPM_DIRECTORY; 77 | fi 78 | 79 | if [ "$NODE_ENV" != "production" ]; then 80 | 81 | echo "---> Building your Node application from source" 82 | npm install 83 | 84 | else 85 | 86 | echo "---> Installing all dependencies" 87 | NODE_ENV=development npm install 88 | 89 | #do not fail when there is no build script 90 | echo "---> Building in production mode" 91 | npm run build --if-present 92 | 93 | echo "---> Pruning the development dependencies" 94 | npm prune 95 | 96 | # Clear the npm's cache and tmp directories only if they are not a docker volumes 97 | NPM_CACHE=$(npm config get cache) 98 | if ! mountpoint $NPM_CACHE; then 99 | echo "---> Cleaning the npm cache $NPM_CACHE" 100 | #As of npm@5 even the 'npm cache clean --force' does not fully remove the cache directory 101 | rm $NPM_CACHE* -rf 102 | fi 103 | NPM_TMP=$(npm config get tmp) 104 | if ! mountpoint $NPM_TMP; then 105 | echo "---> Cleaning the $NPM_TMP/npm-*" 106 | rm -rf $NPM_TMP/npm-* 107 | fi 108 | 109 | fi 110 | 111 | # Fix source directory permissions 112 | fix-permissions ./ 113 | -------------------------------------------------------------------------------- /.s2i/bin/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # S2I run script for the 'nodejs' image. 4 | # The run script executes the server that runs your application. 5 | # 6 | # For more information see the documentation: 7 | # https://github.com/openshift/source-to-image/blob/master/docs/builder_image.md 8 | # 9 | 10 | set -e 11 | 12 | if [ -e "/opt/app-root/etc/generate_container_user" ]; then 13 | source /opt/app-root/etc/generate_container_user 14 | fi 15 | 16 | # If NPM_DIRECTORY is set, change the directory before installing and building 17 | if [ ! -z "$NPM_DIRECTORY" ]; then 18 | echo "Setting subdirectory to " $(safeLogging $NPM_DIRECTORY) 19 | cd $NPM_DIRECTORY; 20 | fi 21 | 22 | # Runs the nodejs application server. If the container is run in development mode, 23 | # hot deploy and debugging are enabled. 24 | run_node() { 25 | echo -e "Environment: \n\tDEV_MODE=${DEV_MODE}\n\tNODE_ENV=${NODE_ENV}\n\tDEBUG_PORT=${DEBUG_PORT}" 26 | if [ "$DEV_MODE" == true ]; then 27 | echo "Launching via nodemon..." 28 | exec nodemon --inspect="$DEBUG_PORT" 29 | else 30 | echo "Launching via npm..." 31 | exec npm run -d $NPM_RUN 32 | fi 33 | } 34 | 35 | #Set the debug port to 5858 by default. 36 | if [ -z "$DEBUG_PORT" ]; then 37 | export DEBUG_PORT=5858 38 | fi 39 | 40 | # Set the environment to development by default. 41 | if [ -z "$DEV_MODE" ]; then 42 | export DEV_MODE=false 43 | fi 44 | 45 | # If NODE_ENV is not set by the user, then NODE_ENV is determined by whether 46 | # the container is run in development mode. 47 | if [ -z "$NODE_ENV" ]; then 48 | if [ "$DEV_MODE" == true ]; then 49 | export NODE_ENV=development 50 | else 51 | export NODE_ENV=production 52 | fi 53 | fi 54 | 55 | # If the official dockerhub node image is used, skip the SCL setup below 56 | # and just run the nodejs server 57 | if [ -d "/usr/src/app" ]; then 58 | run_node 59 | fi 60 | 61 | # Allow users to inspect/debug the builder image itself, by using: 62 | # $ docker run -i -t openshift/centos-nodejs-builder --debug 63 | # 64 | [ "$1" == "--debug" ] && exec /bin/bash 65 | 66 | run_node 67 | -------------------------------------------------------------------------------- /.s2i/bin/usage: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` 4 | 5 | cat < 0;\n }\n}\n", 9 | "sourcePath": "openzeppelin-solidity/contracts/utils/Address.sol", 10 | "ast": { 11 | "absolutePath": "openzeppelin-solidity/contracts/utils/Address.sol", 12 | "exportedSymbols": { 13 | "Address": [ 14 | 1635 15 | ] 16 | }, 17 | "id": 1636, 18 | "nodeType": "SourceUnit", 19 | "nodes": [ 20 | { 21 | "id": 1618, 22 | "literals": [ 23 | "solidity", 24 | "^", 25 | "0.5", 26 | ".0" 27 | ], 28 | "nodeType": "PragmaDirective", 29 | "src": "0:23:14" 30 | }, 31 | { 32 | "baseContracts": [], 33 | "contractDependencies": [], 34 | "contractKind": "library", 35 | "documentation": "Utility library of inline functions on addresses", 36 | "fullyImplemented": true, 37 | "id": 1635, 38 | "linearizedBaseContracts": [ 39 | 1635 40 | ], 41 | "name": "Address", 42 | "nodeType": "ContractDefinition", 43 | "nodes": [ 44 | { 45 | "body": { 46 | "id": 1633, 47 | "nodeType": "Block", 48 | "src": "529:550:14", 49 | "statements": [ 50 | { 51 | "assignments": [ 52 | 1626 53 | ], 54 | "declarations": [ 55 | { 56 | "constant": false, 57 | "id": 1626, 58 | "name": "size", 59 | "nodeType": "VariableDeclaration", 60 | "scope": 1633, 61 | "src": "539:12:14", 62 | "stateVariable": false, 63 | "storageLocation": "default", 64 | "typeDescriptions": { 65 | "typeIdentifier": "t_uint256", 66 | "typeString": "uint256" 67 | }, 68 | "typeName": { 69 | "id": 1625, 70 | "name": "uint256", 71 | "nodeType": "ElementaryTypeName", 72 | "src": "539:7:14", 73 | "typeDescriptions": { 74 | "typeIdentifier": "t_uint256", 75 | "typeString": "uint256" 76 | } 77 | }, 78 | "value": null, 79 | "visibility": "internal" 80 | } 81 | ], 82 | "id": 1627, 83 | "initialValue": null, 84 | "nodeType": "VariableDeclarationStatement", 85 | "src": "539:12:14" 86 | }, 87 | { 88 | "externalReferences": [ 89 | { 90 | "size": { 91 | "declaration": 1626, 92 | "isOffset": false, 93 | "isSlot": false, 94 | "src": "1018:4:14", 95 | "valueSize": 1 96 | } 97 | }, 98 | { 99 | "account": { 100 | "declaration": 1620, 101 | "isOffset": false, 102 | "isSlot": false, 103 | "src": "1038:7:14", 104 | "valueSize": 1 105 | } 106 | } 107 | ], 108 | "id": 1628, 109 | "nodeType": "InlineAssembly", 110 | "operations": "{\n size := extcodesize(account)\n}", 111 | "src": "1007:56:14" 112 | }, 113 | { 114 | "expression": { 115 | "argumentTypes": null, 116 | "commonType": { 117 | "typeIdentifier": "t_uint256", 118 | "typeString": "uint256" 119 | }, 120 | "id": 1631, 121 | "isConstant": false, 122 | "isLValue": false, 123 | "isPure": false, 124 | "lValueRequested": false, 125 | "leftExpression": { 126 | "argumentTypes": null, 127 | "id": 1629, 128 | "name": "size", 129 | "nodeType": "Identifier", 130 | "overloadedDeclarations": [], 131 | "referencedDeclaration": 1626, 132 | "src": "1064:4:14", 133 | "typeDescriptions": { 134 | "typeIdentifier": "t_uint256", 135 | "typeString": "uint256" 136 | } 137 | }, 138 | "nodeType": "BinaryOperation", 139 | "operator": ">", 140 | "rightExpression": { 141 | "argumentTypes": null, 142 | "hexValue": "30", 143 | "id": 1630, 144 | "isConstant": false, 145 | "isLValue": false, 146 | "isPure": true, 147 | "kind": "number", 148 | "lValueRequested": false, 149 | "nodeType": "Literal", 150 | "src": "1071:1:14", 151 | "subdenomination": null, 152 | "typeDescriptions": { 153 | "typeIdentifier": "t_rational_0_by_1", 154 | "typeString": "int_const 0" 155 | }, 156 | "value": "0" 157 | }, 158 | "src": "1064:8:14", 159 | "typeDescriptions": { 160 | "typeIdentifier": "t_bool", 161 | "typeString": "bool" 162 | } 163 | }, 164 | "functionReturnParameters": 1624, 165 | "id": 1632, 166 | "nodeType": "Return", 167 | "src": "1057:15:14" 168 | } 169 | ] 170 | }, 171 | "documentation": "Returns whether the target address is a contract\n@dev This function will return false if invoked during the constructor of a contract,\nas the code is not actually created until after the constructor finishes.\n@param account address of the account to check\n@return whether the target address is a contract", 172 | "id": 1634, 173 | "implemented": true, 174 | "kind": "function", 175 | "modifiers": [], 176 | "name": "isContract", 177 | "nodeType": "FunctionDefinition", 178 | "parameters": { 179 | "id": 1621, 180 | "nodeType": "ParameterList", 181 | "parameters": [ 182 | { 183 | "constant": false, 184 | "id": 1620, 185 | "name": "account", 186 | "nodeType": "VariableDeclaration", 187 | "scope": 1634, 188 | "src": "483:15:14", 189 | "stateVariable": false, 190 | "storageLocation": "default", 191 | "typeDescriptions": { 192 | "typeIdentifier": "t_address", 193 | "typeString": "address" 194 | }, 195 | "typeName": { 196 | "id": 1619, 197 | "name": "address", 198 | "nodeType": "ElementaryTypeName", 199 | "src": "483:7:14", 200 | "stateMutability": "nonpayable", 201 | "typeDescriptions": { 202 | "typeIdentifier": "t_address", 203 | "typeString": "address" 204 | } 205 | }, 206 | "value": null, 207 | "visibility": "internal" 208 | } 209 | ], 210 | "src": "482:17:14" 211 | }, 212 | "returnParameters": { 213 | "id": 1624, 214 | "nodeType": "ParameterList", 215 | "parameters": [ 216 | { 217 | "constant": false, 218 | "id": 1623, 219 | "name": "", 220 | "nodeType": "VariableDeclaration", 221 | "scope": 1634, 222 | "src": "523:4:14", 223 | "stateVariable": false, 224 | "storageLocation": "default", 225 | "typeDescriptions": { 226 | "typeIdentifier": "t_bool", 227 | "typeString": "bool" 228 | }, 229 | "typeName": { 230 | "id": 1622, 231 | "name": "bool", 232 | "nodeType": "ElementaryTypeName", 233 | "src": "523:4:14", 234 | "typeDescriptions": { 235 | "typeIdentifier": "t_bool", 236 | "typeString": "bool" 237 | } 238 | }, 239 | "value": null, 240 | "visibility": "internal" 241 | } 242 | ], 243 | "src": "522:6:14" 244 | }, 245 | "scope": 1635, 246 | "src": "463:616:14", 247 | "stateMutability": "view", 248 | "superFunction": null, 249 | "visibility": "internal" 250 | } 251 | ], 252 | "scope": 1636, 253 | "src": "85:996:14" 254 | } 255 | ], 256 | "src": "0:1082:14" 257 | }, 258 | "legacyAST": { 259 | "absolutePath": "openzeppelin-solidity/contracts/utils/Address.sol", 260 | "exportedSymbols": { 261 | "Address": [ 262 | 1635 263 | ] 264 | }, 265 | "id": 1636, 266 | "nodeType": "SourceUnit", 267 | "nodes": [ 268 | { 269 | "id": 1618, 270 | "literals": [ 271 | "solidity", 272 | "^", 273 | "0.5", 274 | ".0" 275 | ], 276 | "nodeType": "PragmaDirective", 277 | "src": "0:23:14" 278 | }, 279 | { 280 | "baseContracts": [], 281 | "contractDependencies": [], 282 | "contractKind": "library", 283 | "documentation": "Utility library of inline functions on addresses", 284 | "fullyImplemented": true, 285 | "id": 1635, 286 | "linearizedBaseContracts": [ 287 | 1635 288 | ], 289 | "name": "Address", 290 | "nodeType": "ContractDefinition", 291 | "nodes": [ 292 | { 293 | "body": { 294 | "id": 1633, 295 | "nodeType": "Block", 296 | "src": "529:550:14", 297 | "statements": [ 298 | { 299 | "assignments": [ 300 | 1626 301 | ], 302 | "declarations": [ 303 | { 304 | "constant": false, 305 | "id": 1626, 306 | "name": "size", 307 | "nodeType": "VariableDeclaration", 308 | "scope": 1633, 309 | "src": "539:12:14", 310 | "stateVariable": false, 311 | "storageLocation": "default", 312 | "typeDescriptions": { 313 | "typeIdentifier": "t_uint256", 314 | "typeString": "uint256" 315 | }, 316 | "typeName": { 317 | "id": 1625, 318 | "name": "uint256", 319 | "nodeType": "ElementaryTypeName", 320 | "src": "539:7:14", 321 | "typeDescriptions": { 322 | "typeIdentifier": "t_uint256", 323 | "typeString": "uint256" 324 | } 325 | }, 326 | "value": null, 327 | "visibility": "internal" 328 | } 329 | ], 330 | "id": 1627, 331 | "initialValue": null, 332 | "nodeType": "VariableDeclarationStatement", 333 | "src": "539:12:14" 334 | }, 335 | { 336 | "externalReferences": [ 337 | { 338 | "size": { 339 | "declaration": 1626, 340 | "isOffset": false, 341 | "isSlot": false, 342 | "src": "1018:4:14", 343 | "valueSize": 1 344 | } 345 | }, 346 | { 347 | "account": { 348 | "declaration": 1620, 349 | "isOffset": false, 350 | "isSlot": false, 351 | "src": "1038:7:14", 352 | "valueSize": 1 353 | } 354 | } 355 | ], 356 | "id": 1628, 357 | "nodeType": "InlineAssembly", 358 | "operations": "{\n size := extcodesize(account)\n}", 359 | "src": "1007:56:14" 360 | }, 361 | { 362 | "expression": { 363 | "argumentTypes": null, 364 | "commonType": { 365 | "typeIdentifier": "t_uint256", 366 | "typeString": "uint256" 367 | }, 368 | "id": 1631, 369 | "isConstant": false, 370 | "isLValue": false, 371 | "isPure": false, 372 | "lValueRequested": false, 373 | "leftExpression": { 374 | "argumentTypes": null, 375 | "id": 1629, 376 | "name": "size", 377 | "nodeType": "Identifier", 378 | "overloadedDeclarations": [], 379 | "referencedDeclaration": 1626, 380 | "src": "1064:4:14", 381 | "typeDescriptions": { 382 | "typeIdentifier": "t_uint256", 383 | "typeString": "uint256" 384 | } 385 | }, 386 | "nodeType": "BinaryOperation", 387 | "operator": ">", 388 | "rightExpression": { 389 | "argumentTypes": null, 390 | "hexValue": "30", 391 | "id": 1630, 392 | "isConstant": false, 393 | "isLValue": false, 394 | "isPure": true, 395 | "kind": "number", 396 | "lValueRequested": false, 397 | "nodeType": "Literal", 398 | "src": "1071:1:14", 399 | "subdenomination": null, 400 | "typeDescriptions": { 401 | "typeIdentifier": "t_rational_0_by_1", 402 | "typeString": "int_const 0" 403 | }, 404 | "value": "0" 405 | }, 406 | "src": "1064:8:14", 407 | "typeDescriptions": { 408 | "typeIdentifier": "t_bool", 409 | "typeString": "bool" 410 | } 411 | }, 412 | "functionReturnParameters": 1624, 413 | "id": 1632, 414 | "nodeType": "Return", 415 | "src": "1057:15:14" 416 | } 417 | ] 418 | }, 419 | "documentation": "Returns whether the target address is a contract\n@dev This function will return false if invoked during the constructor of a contract,\nas the code is not actually created until after the constructor finishes.\n@param account address of the account to check\n@return whether the target address is a contract", 420 | "id": 1634, 421 | "implemented": true, 422 | "kind": "function", 423 | "modifiers": [], 424 | "name": "isContract", 425 | "nodeType": "FunctionDefinition", 426 | "parameters": { 427 | "id": 1621, 428 | "nodeType": "ParameterList", 429 | "parameters": [ 430 | { 431 | "constant": false, 432 | "id": 1620, 433 | "name": "account", 434 | "nodeType": "VariableDeclaration", 435 | "scope": 1634, 436 | "src": "483:15:14", 437 | "stateVariable": false, 438 | "storageLocation": "default", 439 | "typeDescriptions": { 440 | "typeIdentifier": "t_address", 441 | "typeString": "address" 442 | }, 443 | "typeName": { 444 | "id": 1619, 445 | "name": "address", 446 | "nodeType": "ElementaryTypeName", 447 | "src": "483:7:14", 448 | "stateMutability": "nonpayable", 449 | "typeDescriptions": { 450 | "typeIdentifier": "t_address", 451 | "typeString": "address" 452 | } 453 | }, 454 | "value": null, 455 | "visibility": "internal" 456 | } 457 | ], 458 | "src": "482:17:14" 459 | }, 460 | "returnParameters": { 461 | "id": 1624, 462 | "nodeType": "ParameterList", 463 | "parameters": [ 464 | { 465 | "constant": false, 466 | "id": 1623, 467 | "name": "", 468 | "nodeType": "VariableDeclaration", 469 | "scope": 1634, 470 | "src": "523:4:14", 471 | "stateVariable": false, 472 | "storageLocation": "default", 473 | "typeDescriptions": { 474 | "typeIdentifier": "t_bool", 475 | "typeString": "bool" 476 | }, 477 | "typeName": { 478 | "id": 1622, 479 | "name": "bool", 480 | "nodeType": "ElementaryTypeName", 481 | "src": "523:4:14", 482 | "typeDescriptions": { 483 | "typeIdentifier": "t_bool", 484 | "typeString": "bool" 485 | } 486 | }, 487 | "value": null, 488 | "visibility": "internal" 489 | } 490 | ], 491 | "src": "522:6:14" 492 | }, 493 | "scope": 1635, 494 | "src": "463:616:14", 495 | "stateMutability": "view", 496 | "superFunction": null, 497 | "visibility": "internal" 498 | } 499 | ], 500 | "scope": 1636, 501 | "src": "85:996:14" 502 | } 503 | ], 504 | "src": "0:1082:14" 505 | }, 506 | "compiler": { 507 | "name": "solc", 508 | "version": "0.5.0+commit.1d4f565a.Emscripten.clang" 509 | }, 510 | "networks": {}, 511 | "schemaVersion": "3.0.1", 512 | "updatedAt": "2019-01-08T23:23:58.049Z", 513 | "devdoc": { 514 | "methods": {} 515 | }, 516 | "userdoc": { 517 | "methods": {}, 518 | "notice": "Utility library of inline functions on addresses" 519 | } 520 | } -------------------------------------------------------------------------------- /build/contracts/IERC165.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "IERC165", 3 | "abi": [ 4 | { 5 | "constant": true, 6 | "inputs": [ 7 | { 8 | "name": "interfaceId", 9 | "type": "bytes4" 10 | } 11 | ], 12 | "name": "supportsInterface", 13 | "outputs": [ 14 | { 15 | "name": "", 16 | "type": "bool" 17 | } 18 | ], 19 | "payable": false, 20 | "stateMutability": "view", 21 | "type": "function" 22 | } 23 | ], 24 | "bytecode": "0x", 25 | "deployedBytecode": "0x", 26 | "sourceMap": "", 27 | "deployedSourceMap": "", 28 | "source": "pragma solidity ^0.5.0;\n\n/**\n * @title IERC165\n * @dev https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md\n */\ninterface IERC165 {\n /**\n * @notice Query if a contract implements an interface\n * @param interfaceId The interface identifier, as specified in ERC-165\n * @dev Interface identification is specified in ERC-165. This function\n * uses less than 30,000 gas.\n */\n function supportsInterface(bytes4 interfaceId) external view returns (bool);\n}\n", 29 | "sourcePath": "openzeppelin-solidity/contracts/introspection/IERC165.sol", 30 | "ast": { 31 | "absolutePath": "openzeppelin-solidity/contracts/introspection/IERC165.sol", 32 | "exportedSymbols": { 33 | "IERC165": [ 34 | 250 35 | ] 36 | }, 37 | "id": 251, 38 | "nodeType": "SourceUnit", 39 | "nodes": [ 40 | { 41 | "id": 242, 42 | "literals": [ 43 | "solidity", 44 | "^", 45 | "0.5", 46 | ".0" 47 | ], 48 | "nodeType": "PragmaDirective", 49 | "src": "0:23:3" 50 | }, 51 | { 52 | "baseContracts": [], 53 | "contractDependencies": [], 54 | "contractKind": "interface", 55 | "documentation": "@title IERC165\n@dev https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md", 56 | "fullyImplemented": false, 57 | "id": 250, 58 | "linearizedBaseContracts": [ 59 | 250 60 | ], 61 | "name": "IERC165", 62 | "nodeType": "ContractDefinition", 63 | "nodes": [ 64 | { 65 | "body": null, 66 | "documentation": "@notice Query if a contract implements an interface\n@param interfaceId The interface identifier, as specified in ERC-165\n@dev Interface identification is specified in ERC-165. This function\nuses less than 30,000 gas.", 67 | "id": 249, 68 | "implemented": false, 69 | "kind": "function", 70 | "modifiers": [], 71 | "name": "supportsInterface", 72 | "nodeType": "FunctionDefinition", 73 | "parameters": { 74 | "id": 245, 75 | "nodeType": "ParameterList", 76 | "parameters": [ 77 | { 78 | "constant": false, 79 | "id": 244, 80 | "name": "interfaceId", 81 | "nodeType": "VariableDeclaration", 82 | "scope": 249, 83 | "src": "432:18:3", 84 | "stateVariable": false, 85 | "storageLocation": "default", 86 | "typeDescriptions": { 87 | "typeIdentifier": "t_bytes4", 88 | "typeString": "bytes4" 89 | }, 90 | "typeName": { 91 | "id": 243, 92 | "name": "bytes4", 93 | "nodeType": "ElementaryTypeName", 94 | "src": "432:6:3", 95 | "typeDescriptions": { 96 | "typeIdentifier": "t_bytes4", 97 | "typeString": "bytes4" 98 | } 99 | }, 100 | "value": null, 101 | "visibility": "internal" 102 | } 103 | ], 104 | "src": "431:20:3" 105 | }, 106 | "returnParameters": { 107 | "id": 248, 108 | "nodeType": "ParameterList", 109 | "parameters": [ 110 | { 111 | "constant": false, 112 | "id": 247, 113 | "name": "", 114 | "nodeType": "VariableDeclaration", 115 | "scope": 249, 116 | "src": "475:4:3", 117 | "stateVariable": false, 118 | "storageLocation": "default", 119 | "typeDescriptions": { 120 | "typeIdentifier": "t_bool", 121 | "typeString": "bool" 122 | }, 123 | "typeName": { 124 | "id": 246, 125 | "name": "bool", 126 | "nodeType": "ElementaryTypeName", 127 | "src": "475:4:3", 128 | "typeDescriptions": { 129 | "typeIdentifier": "t_bool", 130 | "typeString": "bool" 131 | } 132 | }, 133 | "value": null, 134 | "visibility": "internal" 135 | } 136 | ], 137 | "src": "474:6:3" 138 | }, 139 | "scope": 250, 140 | "src": "405:76:3", 141 | "stateMutability": "view", 142 | "superFunction": null, 143 | "visibility": "external" 144 | } 145 | ], 146 | "scope": 251, 147 | "src": "120:363:3" 148 | } 149 | ], 150 | "src": "0:484:3" 151 | }, 152 | "legacyAST": { 153 | "absolutePath": "openzeppelin-solidity/contracts/introspection/IERC165.sol", 154 | "exportedSymbols": { 155 | "IERC165": [ 156 | 250 157 | ] 158 | }, 159 | "id": 251, 160 | "nodeType": "SourceUnit", 161 | "nodes": [ 162 | { 163 | "id": 242, 164 | "literals": [ 165 | "solidity", 166 | "^", 167 | "0.5", 168 | ".0" 169 | ], 170 | "nodeType": "PragmaDirective", 171 | "src": "0:23:3" 172 | }, 173 | { 174 | "baseContracts": [], 175 | "contractDependencies": [], 176 | "contractKind": "interface", 177 | "documentation": "@title IERC165\n@dev https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md", 178 | "fullyImplemented": false, 179 | "id": 250, 180 | "linearizedBaseContracts": [ 181 | 250 182 | ], 183 | "name": "IERC165", 184 | "nodeType": "ContractDefinition", 185 | "nodes": [ 186 | { 187 | "body": null, 188 | "documentation": "@notice Query if a contract implements an interface\n@param interfaceId The interface identifier, as specified in ERC-165\n@dev Interface identification is specified in ERC-165. This function\nuses less than 30,000 gas.", 189 | "id": 249, 190 | "implemented": false, 191 | "kind": "function", 192 | "modifiers": [], 193 | "name": "supportsInterface", 194 | "nodeType": "FunctionDefinition", 195 | "parameters": { 196 | "id": 245, 197 | "nodeType": "ParameterList", 198 | "parameters": [ 199 | { 200 | "constant": false, 201 | "id": 244, 202 | "name": "interfaceId", 203 | "nodeType": "VariableDeclaration", 204 | "scope": 249, 205 | "src": "432:18:3", 206 | "stateVariable": false, 207 | "storageLocation": "default", 208 | "typeDescriptions": { 209 | "typeIdentifier": "t_bytes4", 210 | "typeString": "bytes4" 211 | }, 212 | "typeName": { 213 | "id": 243, 214 | "name": "bytes4", 215 | "nodeType": "ElementaryTypeName", 216 | "src": "432:6:3", 217 | "typeDescriptions": { 218 | "typeIdentifier": "t_bytes4", 219 | "typeString": "bytes4" 220 | } 221 | }, 222 | "value": null, 223 | "visibility": "internal" 224 | } 225 | ], 226 | "src": "431:20:3" 227 | }, 228 | "returnParameters": { 229 | "id": 248, 230 | "nodeType": "ParameterList", 231 | "parameters": [ 232 | { 233 | "constant": false, 234 | "id": 247, 235 | "name": "", 236 | "nodeType": "VariableDeclaration", 237 | "scope": 249, 238 | "src": "475:4:3", 239 | "stateVariable": false, 240 | "storageLocation": "default", 241 | "typeDescriptions": { 242 | "typeIdentifier": "t_bool", 243 | "typeString": "bool" 244 | }, 245 | "typeName": { 246 | "id": 246, 247 | "name": "bool", 248 | "nodeType": "ElementaryTypeName", 249 | "src": "475:4:3", 250 | "typeDescriptions": { 251 | "typeIdentifier": "t_bool", 252 | "typeString": "bool" 253 | } 254 | }, 255 | "value": null, 256 | "visibility": "internal" 257 | } 258 | ], 259 | "src": "474:6:3" 260 | }, 261 | "scope": 250, 262 | "src": "405:76:3", 263 | "stateMutability": "view", 264 | "superFunction": null, 265 | "visibility": "external" 266 | } 267 | ], 268 | "scope": 251, 269 | "src": "120:363:3" 270 | } 271 | ], 272 | "src": "0:484:3" 273 | }, 274 | "compiler": { 275 | "name": "solc", 276 | "version": "0.5.0+commit.1d4f565a.Emscripten.clang" 277 | }, 278 | "networks": {}, 279 | "schemaVersion": "3.0.1", 280 | "updatedAt": "2019-01-08T23:23:58.043Z", 281 | "devdoc": { 282 | "details": "https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md", 283 | "methods": { 284 | "supportsInterface(bytes4)": { 285 | "details": "Interface identification is specified in ERC-165. This function uses less than 30,000 gas.", 286 | "params": { 287 | "interfaceId": "The interface identifier, as specified in ERC-165" 288 | } 289 | } 290 | }, 291 | "title": "IERC165" 292 | }, 293 | "userdoc": { 294 | "methods": { 295 | "supportsInterface(bytes4)": { 296 | "notice": "Query if a contract implements an interface" 297 | } 298 | } 299 | } 300 | } -------------------------------------------------------------------------------- /build/contracts/IERC721Enumerable.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "IERC721Enumerable", 3 | "abi": [ 4 | { 5 | "constant": true, 6 | "inputs": [ 7 | { 8 | "name": "interfaceId", 9 | "type": "bytes4" 10 | } 11 | ], 12 | "name": "supportsInterface", 13 | "outputs": [ 14 | { 15 | "name": "", 16 | "type": "bool" 17 | } 18 | ], 19 | "payable": false, 20 | "stateMutability": "view", 21 | "type": "function" 22 | }, 23 | { 24 | "constant": true, 25 | "inputs": [ 26 | { 27 | "name": "tokenId", 28 | "type": "uint256" 29 | } 30 | ], 31 | "name": "getApproved", 32 | "outputs": [ 33 | { 34 | "name": "operator", 35 | "type": "address" 36 | } 37 | ], 38 | "payable": false, 39 | "stateMutability": "view", 40 | "type": "function" 41 | }, 42 | { 43 | "constant": false, 44 | "inputs": [ 45 | { 46 | "name": "to", 47 | "type": "address" 48 | }, 49 | { 50 | "name": "tokenId", 51 | "type": "uint256" 52 | } 53 | ], 54 | "name": "approve", 55 | "outputs": [], 56 | "payable": false, 57 | "stateMutability": "nonpayable", 58 | "type": "function" 59 | }, 60 | { 61 | "constant": false, 62 | "inputs": [ 63 | { 64 | "name": "from", 65 | "type": "address" 66 | }, 67 | { 68 | "name": "to", 69 | "type": "address" 70 | }, 71 | { 72 | "name": "tokenId", 73 | "type": "uint256" 74 | } 75 | ], 76 | "name": "transferFrom", 77 | "outputs": [], 78 | "payable": false, 79 | "stateMutability": "nonpayable", 80 | "type": "function" 81 | }, 82 | { 83 | "constant": false, 84 | "inputs": [ 85 | { 86 | "name": "from", 87 | "type": "address" 88 | }, 89 | { 90 | "name": "to", 91 | "type": "address" 92 | }, 93 | { 94 | "name": "tokenId", 95 | "type": "uint256" 96 | } 97 | ], 98 | "name": "safeTransferFrom", 99 | "outputs": [], 100 | "payable": false, 101 | "stateMutability": "nonpayable", 102 | "type": "function" 103 | }, 104 | { 105 | "constant": true, 106 | "inputs": [ 107 | { 108 | "name": "tokenId", 109 | "type": "uint256" 110 | } 111 | ], 112 | "name": "ownerOf", 113 | "outputs": [ 114 | { 115 | "name": "owner", 116 | "type": "address" 117 | } 118 | ], 119 | "payable": false, 120 | "stateMutability": "view", 121 | "type": "function" 122 | }, 123 | { 124 | "constant": true, 125 | "inputs": [ 126 | { 127 | "name": "owner", 128 | "type": "address" 129 | } 130 | ], 131 | "name": "balanceOf", 132 | "outputs": [ 133 | { 134 | "name": "balance", 135 | "type": "uint256" 136 | } 137 | ], 138 | "payable": false, 139 | "stateMutability": "view", 140 | "type": "function" 141 | }, 142 | { 143 | "constant": false, 144 | "inputs": [ 145 | { 146 | "name": "operator", 147 | "type": "address" 148 | }, 149 | { 150 | "name": "_approved", 151 | "type": "bool" 152 | } 153 | ], 154 | "name": "setApprovalForAll", 155 | "outputs": [], 156 | "payable": false, 157 | "stateMutability": "nonpayable", 158 | "type": "function" 159 | }, 160 | { 161 | "constant": false, 162 | "inputs": [ 163 | { 164 | "name": "from", 165 | "type": "address" 166 | }, 167 | { 168 | "name": "to", 169 | "type": "address" 170 | }, 171 | { 172 | "name": "tokenId", 173 | "type": "uint256" 174 | }, 175 | { 176 | "name": "data", 177 | "type": "bytes" 178 | } 179 | ], 180 | "name": "safeTransferFrom", 181 | "outputs": [], 182 | "payable": false, 183 | "stateMutability": "nonpayable", 184 | "type": "function" 185 | }, 186 | { 187 | "constant": true, 188 | "inputs": [ 189 | { 190 | "name": "owner", 191 | "type": "address" 192 | }, 193 | { 194 | "name": "operator", 195 | "type": "address" 196 | } 197 | ], 198 | "name": "isApprovedForAll", 199 | "outputs": [ 200 | { 201 | "name": "", 202 | "type": "bool" 203 | } 204 | ], 205 | "payable": false, 206 | "stateMutability": "view", 207 | "type": "function" 208 | }, 209 | { 210 | "anonymous": false, 211 | "inputs": [ 212 | { 213 | "indexed": true, 214 | "name": "from", 215 | "type": "address" 216 | }, 217 | { 218 | "indexed": true, 219 | "name": "to", 220 | "type": "address" 221 | }, 222 | { 223 | "indexed": true, 224 | "name": "tokenId", 225 | "type": "uint256" 226 | } 227 | ], 228 | "name": "Transfer", 229 | "type": "event" 230 | }, 231 | { 232 | "anonymous": false, 233 | "inputs": [ 234 | { 235 | "indexed": true, 236 | "name": "owner", 237 | "type": "address" 238 | }, 239 | { 240 | "indexed": true, 241 | "name": "approved", 242 | "type": "address" 243 | }, 244 | { 245 | "indexed": true, 246 | "name": "tokenId", 247 | "type": "uint256" 248 | } 249 | ], 250 | "name": "Approval", 251 | "type": "event" 252 | }, 253 | { 254 | "anonymous": false, 255 | "inputs": [ 256 | { 257 | "indexed": true, 258 | "name": "owner", 259 | "type": "address" 260 | }, 261 | { 262 | "indexed": true, 263 | "name": "operator", 264 | "type": "address" 265 | }, 266 | { 267 | "indexed": false, 268 | "name": "approved", 269 | "type": "bool" 270 | } 271 | ], 272 | "name": "ApprovalForAll", 273 | "type": "event" 274 | }, 275 | { 276 | "constant": true, 277 | "inputs": [], 278 | "name": "totalSupply", 279 | "outputs": [ 280 | { 281 | "name": "", 282 | "type": "uint256" 283 | } 284 | ], 285 | "payable": false, 286 | "stateMutability": "view", 287 | "type": "function" 288 | }, 289 | { 290 | "constant": true, 291 | "inputs": [ 292 | { 293 | "name": "owner", 294 | "type": "address" 295 | }, 296 | { 297 | "name": "index", 298 | "type": "uint256" 299 | } 300 | ], 301 | "name": "tokenOfOwnerByIndex", 302 | "outputs": [ 303 | { 304 | "name": "tokenId", 305 | "type": "uint256" 306 | } 307 | ], 308 | "payable": false, 309 | "stateMutability": "view", 310 | "type": "function" 311 | }, 312 | { 313 | "constant": true, 314 | "inputs": [ 315 | { 316 | "name": "index", 317 | "type": "uint256" 318 | } 319 | ], 320 | "name": "tokenByIndex", 321 | "outputs": [ 322 | { 323 | "name": "", 324 | "type": "uint256" 325 | } 326 | ], 327 | "payable": false, 328 | "stateMutability": "view", 329 | "type": "function" 330 | } 331 | ], 332 | "bytecode": "0x", 333 | "deployedBytecode": "0x", 334 | "sourceMap": "", 335 | "deployedSourceMap": "", 336 | "source": "pragma solidity ^0.5.0;\n\nimport \"./IERC721.sol\";\n\n/**\n * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension\n * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md\n */\ncontract IERC721Enumerable is IERC721 {\n function totalSupply() public view returns (uint256);\n function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256 tokenId);\n\n function tokenByIndex(uint256 index) public view returns (uint256);\n}\n", 337 | "sourcePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721Enumerable.sol", 338 | "ast": { 339 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721Enumerable.sol", 340 | "exportedSymbols": { 341 | "IERC721Enumerable": [ 342 | 1577 343 | ] 344 | }, 345 | "id": 1578, 346 | "nodeType": "SourceUnit", 347 | "nodes": [ 348 | { 349 | "id": 1552, 350 | "literals": [ 351 | "solidity", 352 | "^", 353 | "0.5", 354 | ".0" 355 | ], 356 | "nodeType": "PragmaDirective", 357 | "src": "0:23:11" 358 | }, 359 | { 360 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721.sol", 361 | "file": "./IERC721.sol", 362 | "id": 1553, 363 | "nodeType": "ImportDirective", 364 | "scope": 1578, 365 | "sourceUnit": 1551, 366 | "src": "25:23:11", 367 | "symbolAliases": [], 368 | "unitAlias": "" 369 | }, 370 | { 371 | "baseContracts": [ 372 | { 373 | "arguments": null, 374 | "baseName": { 375 | "contractScope": null, 376 | "id": 1554, 377 | "name": "IERC721", 378 | "nodeType": "UserDefinedTypeName", 379 | "referencedDeclaration": 1550, 380 | "src": "239:7:11", 381 | "typeDescriptions": { 382 | "typeIdentifier": "t_contract$_IERC721_$1550", 383 | "typeString": "contract IERC721" 384 | } 385 | }, 386 | "id": 1555, 387 | "nodeType": "InheritanceSpecifier", 388 | "src": "239:7:11" 389 | } 390 | ], 391 | "contractDependencies": [ 392 | 250, 393 | 1550 394 | ], 395 | "contractKind": "contract", 396 | "documentation": "@title ERC-721 Non-Fungible Token Standard, optional enumeration extension\n@dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md", 397 | "fullyImplemented": false, 398 | "id": 1577, 399 | "linearizedBaseContracts": [ 400 | 1577, 401 | 1550, 402 | 250 403 | ], 404 | "name": "IERC721Enumerable", 405 | "nodeType": "ContractDefinition", 406 | "nodes": [ 407 | { 408 | "body": null, 409 | "documentation": null, 410 | "id": 1560, 411 | "implemented": false, 412 | "kind": "function", 413 | "modifiers": [], 414 | "name": "totalSupply", 415 | "nodeType": "FunctionDefinition", 416 | "parameters": { 417 | "id": 1556, 418 | "nodeType": "ParameterList", 419 | "parameters": [], 420 | "src": "273:2:11" 421 | }, 422 | "returnParameters": { 423 | "id": 1559, 424 | "nodeType": "ParameterList", 425 | "parameters": [ 426 | { 427 | "constant": false, 428 | "id": 1558, 429 | "name": "", 430 | "nodeType": "VariableDeclaration", 431 | "scope": 1560, 432 | "src": "297:7:11", 433 | "stateVariable": false, 434 | "storageLocation": "default", 435 | "typeDescriptions": { 436 | "typeIdentifier": "t_uint256", 437 | "typeString": "uint256" 438 | }, 439 | "typeName": { 440 | "id": 1557, 441 | "name": "uint256", 442 | "nodeType": "ElementaryTypeName", 443 | "src": "297:7:11", 444 | "typeDescriptions": { 445 | "typeIdentifier": "t_uint256", 446 | "typeString": "uint256" 447 | } 448 | }, 449 | "value": null, 450 | "visibility": "internal" 451 | } 452 | ], 453 | "src": "296:9:11" 454 | }, 455 | "scope": 1577, 456 | "src": "253:53:11", 457 | "stateMutability": "view", 458 | "superFunction": null, 459 | "visibility": "public" 460 | }, 461 | { 462 | "body": null, 463 | "documentation": null, 464 | "id": 1569, 465 | "implemented": false, 466 | "kind": "function", 467 | "modifiers": [], 468 | "name": "tokenOfOwnerByIndex", 469 | "nodeType": "FunctionDefinition", 470 | "parameters": { 471 | "id": 1565, 472 | "nodeType": "ParameterList", 473 | "parameters": [ 474 | { 475 | "constant": false, 476 | "id": 1562, 477 | "name": "owner", 478 | "nodeType": "VariableDeclaration", 479 | "scope": 1569, 480 | "src": "340:13:11", 481 | "stateVariable": false, 482 | "storageLocation": "default", 483 | "typeDescriptions": { 484 | "typeIdentifier": "t_address", 485 | "typeString": "address" 486 | }, 487 | "typeName": { 488 | "id": 1561, 489 | "name": "address", 490 | "nodeType": "ElementaryTypeName", 491 | "src": "340:7:11", 492 | "stateMutability": "nonpayable", 493 | "typeDescriptions": { 494 | "typeIdentifier": "t_address", 495 | "typeString": "address" 496 | } 497 | }, 498 | "value": null, 499 | "visibility": "internal" 500 | }, 501 | { 502 | "constant": false, 503 | "id": 1564, 504 | "name": "index", 505 | "nodeType": "VariableDeclaration", 506 | "scope": 1569, 507 | "src": "355:13:11", 508 | "stateVariable": false, 509 | "storageLocation": "default", 510 | "typeDescriptions": { 511 | "typeIdentifier": "t_uint256", 512 | "typeString": "uint256" 513 | }, 514 | "typeName": { 515 | "id": 1563, 516 | "name": "uint256", 517 | "nodeType": "ElementaryTypeName", 518 | "src": "355:7:11", 519 | "typeDescriptions": { 520 | "typeIdentifier": "t_uint256", 521 | "typeString": "uint256" 522 | } 523 | }, 524 | "value": null, 525 | "visibility": "internal" 526 | } 527 | ], 528 | "src": "339:30:11" 529 | }, 530 | "returnParameters": { 531 | "id": 1568, 532 | "nodeType": "ParameterList", 533 | "parameters": [ 534 | { 535 | "constant": false, 536 | "id": 1567, 537 | "name": "tokenId", 538 | "nodeType": "VariableDeclaration", 539 | "scope": 1569, 540 | "src": "391:15:11", 541 | "stateVariable": false, 542 | "storageLocation": "default", 543 | "typeDescriptions": { 544 | "typeIdentifier": "t_uint256", 545 | "typeString": "uint256" 546 | }, 547 | "typeName": { 548 | "id": 1566, 549 | "name": "uint256", 550 | "nodeType": "ElementaryTypeName", 551 | "src": "391:7:11", 552 | "typeDescriptions": { 553 | "typeIdentifier": "t_uint256", 554 | "typeString": "uint256" 555 | } 556 | }, 557 | "value": null, 558 | "visibility": "internal" 559 | } 560 | ], 561 | "src": "390:17:11" 562 | }, 563 | "scope": 1577, 564 | "src": "311:97:11", 565 | "stateMutability": "view", 566 | "superFunction": null, 567 | "visibility": "public" 568 | }, 569 | { 570 | "body": null, 571 | "documentation": null, 572 | "id": 1576, 573 | "implemented": false, 574 | "kind": "function", 575 | "modifiers": [], 576 | "name": "tokenByIndex", 577 | "nodeType": "FunctionDefinition", 578 | "parameters": { 579 | "id": 1572, 580 | "nodeType": "ParameterList", 581 | "parameters": [ 582 | { 583 | "constant": false, 584 | "id": 1571, 585 | "name": "index", 586 | "nodeType": "VariableDeclaration", 587 | "scope": 1576, 588 | "src": "436:13:11", 589 | "stateVariable": false, 590 | "storageLocation": "default", 591 | "typeDescriptions": { 592 | "typeIdentifier": "t_uint256", 593 | "typeString": "uint256" 594 | }, 595 | "typeName": { 596 | "id": 1570, 597 | "name": "uint256", 598 | "nodeType": "ElementaryTypeName", 599 | "src": "436:7:11", 600 | "typeDescriptions": { 601 | "typeIdentifier": "t_uint256", 602 | "typeString": "uint256" 603 | } 604 | }, 605 | "value": null, 606 | "visibility": "internal" 607 | } 608 | ], 609 | "src": "435:15:11" 610 | }, 611 | "returnParameters": { 612 | "id": 1575, 613 | "nodeType": "ParameterList", 614 | "parameters": [ 615 | { 616 | "constant": false, 617 | "id": 1574, 618 | "name": "", 619 | "nodeType": "VariableDeclaration", 620 | "scope": 1576, 621 | "src": "472:7:11", 622 | "stateVariable": false, 623 | "storageLocation": "default", 624 | "typeDescriptions": { 625 | "typeIdentifier": "t_uint256", 626 | "typeString": "uint256" 627 | }, 628 | "typeName": { 629 | "id": 1573, 630 | "name": "uint256", 631 | "nodeType": "ElementaryTypeName", 632 | "src": "472:7:11", 633 | "typeDescriptions": { 634 | "typeIdentifier": "t_uint256", 635 | "typeString": "uint256" 636 | } 637 | }, 638 | "value": null, 639 | "visibility": "internal" 640 | } 641 | ], 642 | "src": "471:9:11" 643 | }, 644 | "scope": 1577, 645 | "src": "414:67:11", 646 | "stateMutability": "view", 647 | "superFunction": null, 648 | "visibility": "public" 649 | } 650 | ], 651 | "scope": 1578, 652 | "src": "209:274:11" 653 | } 654 | ], 655 | "src": "0:484:11" 656 | }, 657 | "legacyAST": { 658 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721Enumerable.sol", 659 | "exportedSymbols": { 660 | "IERC721Enumerable": [ 661 | 1577 662 | ] 663 | }, 664 | "id": 1578, 665 | "nodeType": "SourceUnit", 666 | "nodes": [ 667 | { 668 | "id": 1552, 669 | "literals": [ 670 | "solidity", 671 | "^", 672 | "0.5", 673 | ".0" 674 | ], 675 | "nodeType": "PragmaDirective", 676 | "src": "0:23:11" 677 | }, 678 | { 679 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721.sol", 680 | "file": "./IERC721.sol", 681 | "id": 1553, 682 | "nodeType": "ImportDirective", 683 | "scope": 1578, 684 | "sourceUnit": 1551, 685 | "src": "25:23:11", 686 | "symbolAliases": [], 687 | "unitAlias": "" 688 | }, 689 | { 690 | "baseContracts": [ 691 | { 692 | "arguments": null, 693 | "baseName": { 694 | "contractScope": null, 695 | "id": 1554, 696 | "name": "IERC721", 697 | "nodeType": "UserDefinedTypeName", 698 | "referencedDeclaration": 1550, 699 | "src": "239:7:11", 700 | "typeDescriptions": { 701 | "typeIdentifier": "t_contract$_IERC721_$1550", 702 | "typeString": "contract IERC721" 703 | } 704 | }, 705 | "id": 1555, 706 | "nodeType": "InheritanceSpecifier", 707 | "src": "239:7:11" 708 | } 709 | ], 710 | "contractDependencies": [ 711 | 250, 712 | 1550 713 | ], 714 | "contractKind": "contract", 715 | "documentation": "@title ERC-721 Non-Fungible Token Standard, optional enumeration extension\n@dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md", 716 | "fullyImplemented": false, 717 | "id": 1577, 718 | "linearizedBaseContracts": [ 719 | 1577, 720 | 1550, 721 | 250 722 | ], 723 | "name": "IERC721Enumerable", 724 | "nodeType": "ContractDefinition", 725 | "nodes": [ 726 | { 727 | "body": null, 728 | "documentation": null, 729 | "id": 1560, 730 | "implemented": false, 731 | "kind": "function", 732 | "modifiers": [], 733 | "name": "totalSupply", 734 | "nodeType": "FunctionDefinition", 735 | "parameters": { 736 | "id": 1556, 737 | "nodeType": "ParameterList", 738 | "parameters": [], 739 | "src": "273:2:11" 740 | }, 741 | "returnParameters": { 742 | "id": 1559, 743 | "nodeType": "ParameterList", 744 | "parameters": [ 745 | { 746 | "constant": false, 747 | "id": 1558, 748 | "name": "", 749 | "nodeType": "VariableDeclaration", 750 | "scope": 1560, 751 | "src": "297:7:11", 752 | "stateVariable": false, 753 | "storageLocation": "default", 754 | "typeDescriptions": { 755 | "typeIdentifier": "t_uint256", 756 | "typeString": "uint256" 757 | }, 758 | "typeName": { 759 | "id": 1557, 760 | "name": "uint256", 761 | "nodeType": "ElementaryTypeName", 762 | "src": "297:7:11", 763 | "typeDescriptions": { 764 | "typeIdentifier": "t_uint256", 765 | "typeString": "uint256" 766 | } 767 | }, 768 | "value": null, 769 | "visibility": "internal" 770 | } 771 | ], 772 | "src": "296:9:11" 773 | }, 774 | "scope": 1577, 775 | "src": "253:53:11", 776 | "stateMutability": "view", 777 | "superFunction": null, 778 | "visibility": "public" 779 | }, 780 | { 781 | "body": null, 782 | "documentation": null, 783 | "id": 1569, 784 | "implemented": false, 785 | "kind": "function", 786 | "modifiers": [], 787 | "name": "tokenOfOwnerByIndex", 788 | "nodeType": "FunctionDefinition", 789 | "parameters": { 790 | "id": 1565, 791 | "nodeType": "ParameterList", 792 | "parameters": [ 793 | { 794 | "constant": false, 795 | "id": 1562, 796 | "name": "owner", 797 | "nodeType": "VariableDeclaration", 798 | "scope": 1569, 799 | "src": "340:13:11", 800 | "stateVariable": false, 801 | "storageLocation": "default", 802 | "typeDescriptions": { 803 | "typeIdentifier": "t_address", 804 | "typeString": "address" 805 | }, 806 | "typeName": { 807 | "id": 1561, 808 | "name": "address", 809 | "nodeType": "ElementaryTypeName", 810 | "src": "340:7:11", 811 | "stateMutability": "nonpayable", 812 | "typeDescriptions": { 813 | "typeIdentifier": "t_address", 814 | "typeString": "address" 815 | } 816 | }, 817 | "value": null, 818 | "visibility": "internal" 819 | }, 820 | { 821 | "constant": false, 822 | "id": 1564, 823 | "name": "index", 824 | "nodeType": "VariableDeclaration", 825 | "scope": 1569, 826 | "src": "355:13:11", 827 | "stateVariable": false, 828 | "storageLocation": "default", 829 | "typeDescriptions": { 830 | "typeIdentifier": "t_uint256", 831 | "typeString": "uint256" 832 | }, 833 | "typeName": { 834 | "id": 1563, 835 | "name": "uint256", 836 | "nodeType": "ElementaryTypeName", 837 | "src": "355:7:11", 838 | "typeDescriptions": { 839 | "typeIdentifier": "t_uint256", 840 | "typeString": "uint256" 841 | } 842 | }, 843 | "value": null, 844 | "visibility": "internal" 845 | } 846 | ], 847 | "src": "339:30:11" 848 | }, 849 | "returnParameters": { 850 | "id": 1568, 851 | "nodeType": "ParameterList", 852 | "parameters": [ 853 | { 854 | "constant": false, 855 | "id": 1567, 856 | "name": "tokenId", 857 | "nodeType": "VariableDeclaration", 858 | "scope": 1569, 859 | "src": "391:15:11", 860 | "stateVariable": false, 861 | "storageLocation": "default", 862 | "typeDescriptions": { 863 | "typeIdentifier": "t_uint256", 864 | "typeString": "uint256" 865 | }, 866 | "typeName": { 867 | "id": 1566, 868 | "name": "uint256", 869 | "nodeType": "ElementaryTypeName", 870 | "src": "391:7:11", 871 | "typeDescriptions": { 872 | "typeIdentifier": "t_uint256", 873 | "typeString": "uint256" 874 | } 875 | }, 876 | "value": null, 877 | "visibility": "internal" 878 | } 879 | ], 880 | "src": "390:17:11" 881 | }, 882 | "scope": 1577, 883 | "src": "311:97:11", 884 | "stateMutability": "view", 885 | "superFunction": null, 886 | "visibility": "public" 887 | }, 888 | { 889 | "body": null, 890 | "documentation": null, 891 | "id": 1576, 892 | "implemented": false, 893 | "kind": "function", 894 | "modifiers": [], 895 | "name": "tokenByIndex", 896 | "nodeType": "FunctionDefinition", 897 | "parameters": { 898 | "id": 1572, 899 | "nodeType": "ParameterList", 900 | "parameters": [ 901 | { 902 | "constant": false, 903 | "id": 1571, 904 | "name": "index", 905 | "nodeType": "VariableDeclaration", 906 | "scope": 1576, 907 | "src": "436:13:11", 908 | "stateVariable": false, 909 | "storageLocation": "default", 910 | "typeDescriptions": { 911 | "typeIdentifier": "t_uint256", 912 | "typeString": "uint256" 913 | }, 914 | "typeName": { 915 | "id": 1570, 916 | "name": "uint256", 917 | "nodeType": "ElementaryTypeName", 918 | "src": "436:7:11", 919 | "typeDescriptions": { 920 | "typeIdentifier": "t_uint256", 921 | "typeString": "uint256" 922 | } 923 | }, 924 | "value": null, 925 | "visibility": "internal" 926 | } 927 | ], 928 | "src": "435:15:11" 929 | }, 930 | "returnParameters": { 931 | "id": 1575, 932 | "nodeType": "ParameterList", 933 | "parameters": [ 934 | { 935 | "constant": false, 936 | "id": 1574, 937 | "name": "", 938 | "nodeType": "VariableDeclaration", 939 | "scope": 1576, 940 | "src": "472:7:11", 941 | "stateVariable": false, 942 | "storageLocation": "default", 943 | "typeDescriptions": { 944 | "typeIdentifier": "t_uint256", 945 | "typeString": "uint256" 946 | }, 947 | "typeName": { 948 | "id": 1573, 949 | "name": "uint256", 950 | "nodeType": "ElementaryTypeName", 951 | "src": "472:7:11", 952 | "typeDescriptions": { 953 | "typeIdentifier": "t_uint256", 954 | "typeString": "uint256" 955 | } 956 | }, 957 | "value": null, 958 | "visibility": "internal" 959 | } 960 | ], 961 | "src": "471:9:11" 962 | }, 963 | "scope": 1577, 964 | "src": "414:67:11", 965 | "stateMutability": "view", 966 | "superFunction": null, 967 | "visibility": "public" 968 | } 969 | ], 970 | "scope": 1578, 971 | "src": "209:274:11" 972 | } 973 | ], 974 | "src": "0:484:11" 975 | }, 976 | "compiler": { 977 | "name": "solc", 978 | "version": "0.5.0+commit.1d4f565a.Emscripten.clang" 979 | }, 980 | "networks": {}, 981 | "schemaVersion": "3.0.1", 982 | "updatedAt": "2019-01-08T23:23:58.049Z", 983 | "devdoc": { 984 | "details": "See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md", 985 | "methods": { 986 | "supportsInterface(bytes4)": { 987 | "details": "Interface identification is specified in ERC-165. This function uses less than 30,000 gas.", 988 | "params": { 989 | "interfaceId": "The interface identifier, as specified in ERC-165" 990 | } 991 | } 992 | }, 993 | "title": "ERC-721 Non-Fungible Token Standard, optional enumeration extension" 994 | }, 995 | "userdoc": { 996 | "methods": { 997 | "supportsInterface(bytes4)": { 998 | "notice": "Query if a contract implements an interface" 999 | } 1000 | } 1001 | } 1002 | } -------------------------------------------------------------------------------- /build/contracts/IERC721Metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "IERC721Metadata", 3 | "abi": [ 4 | { 5 | "constant": true, 6 | "inputs": [ 7 | { 8 | "name": "interfaceId", 9 | "type": "bytes4" 10 | } 11 | ], 12 | "name": "supportsInterface", 13 | "outputs": [ 14 | { 15 | "name": "", 16 | "type": "bool" 17 | } 18 | ], 19 | "payable": false, 20 | "stateMutability": "view", 21 | "type": "function" 22 | }, 23 | { 24 | "constant": true, 25 | "inputs": [ 26 | { 27 | "name": "tokenId", 28 | "type": "uint256" 29 | } 30 | ], 31 | "name": "getApproved", 32 | "outputs": [ 33 | { 34 | "name": "operator", 35 | "type": "address" 36 | } 37 | ], 38 | "payable": false, 39 | "stateMutability": "view", 40 | "type": "function" 41 | }, 42 | { 43 | "constant": false, 44 | "inputs": [ 45 | { 46 | "name": "to", 47 | "type": "address" 48 | }, 49 | { 50 | "name": "tokenId", 51 | "type": "uint256" 52 | } 53 | ], 54 | "name": "approve", 55 | "outputs": [], 56 | "payable": false, 57 | "stateMutability": "nonpayable", 58 | "type": "function" 59 | }, 60 | { 61 | "constant": false, 62 | "inputs": [ 63 | { 64 | "name": "from", 65 | "type": "address" 66 | }, 67 | { 68 | "name": "to", 69 | "type": "address" 70 | }, 71 | { 72 | "name": "tokenId", 73 | "type": "uint256" 74 | } 75 | ], 76 | "name": "transferFrom", 77 | "outputs": [], 78 | "payable": false, 79 | "stateMutability": "nonpayable", 80 | "type": "function" 81 | }, 82 | { 83 | "constant": false, 84 | "inputs": [ 85 | { 86 | "name": "from", 87 | "type": "address" 88 | }, 89 | { 90 | "name": "to", 91 | "type": "address" 92 | }, 93 | { 94 | "name": "tokenId", 95 | "type": "uint256" 96 | } 97 | ], 98 | "name": "safeTransferFrom", 99 | "outputs": [], 100 | "payable": false, 101 | "stateMutability": "nonpayable", 102 | "type": "function" 103 | }, 104 | { 105 | "constant": true, 106 | "inputs": [ 107 | { 108 | "name": "tokenId", 109 | "type": "uint256" 110 | } 111 | ], 112 | "name": "ownerOf", 113 | "outputs": [ 114 | { 115 | "name": "owner", 116 | "type": "address" 117 | } 118 | ], 119 | "payable": false, 120 | "stateMutability": "view", 121 | "type": "function" 122 | }, 123 | { 124 | "constant": true, 125 | "inputs": [ 126 | { 127 | "name": "owner", 128 | "type": "address" 129 | } 130 | ], 131 | "name": "balanceOf", 132 | "outputs": [ 133 | { 134 | "name": "balance", 135 | "type": "uint256" 136 | } 137 | ], 138 | "payable": false, 139 | "stateMutability": "view", 140 | "type": "function" 141 | }, 142 | { 143 | "constant": false, 144 | "inputs": [ 145 | { 146 | "name": "operator", 147 | "type": "address" 148 | }, 149 | { 150 | "name": "_approved", 151 | "type": "bool" 152 | } 153 | ], 154 | "name": "setApprovalForAll", 155 | "outputs": [], 156 | "payable": false, 157 | "stateMutability": "nonpayable", 158 | "type": "function" 159 | }, 160 | { 161 | "constant": false, 162 | "inputs": [ 163 | { 164 | "name": "from", 165 | "type": "address" 166 | }, 167 | { 168 | "name": "to", 169 | "type": "address" 170 | }, 171 | { 172 | "name": "tokenId", 173 | "type": "uint256" 174 | }, 175 | { 176 | "name": "data", 177 | "type": "bytes" 178 | } 179 | ], 180 | "name": "safeTransferFrom", 181 | "outputs": [], 182 | "payable": false, 183 | "stateMutability": "nonpayable", 184 | "type": "function" 185 | }, 186 | { 187 | "constant": true, 188 | "inputs": [ 189 | { 190 | "name": "owner", 191 | "type": "address" 192 | }, 193 | { 194 | "name": "operator", 195 | "type": "address" 196 | } 197 | ], 198 | "name": "isApprovedForAll", 199 | "outputs": [ 200 | { 201 | "name": "", 202 | "type": "bool" 203 | } 204 | ], 205 | "payable": false, 206 | "stateMutability": "view", 207 | "type": "function" 208 | }, 209 | { 210 | "anonymous": false, 211 | "inputs": [ 212 | { 213 | "indexed": true, 214 | "name": "from", 215 | "type": "address" 216 | }, 217 | { 218 | "indexed": true, 219 | "name": "to", 220 | "type": "address" 221 | }, 222 | { 223 | "indexed": true, 224 | "name": "tokenId", 225 | "type": "uint256" 226 | } 227 | ], 228 | "name": "Transfer", 229 | "type": "event" 230 | }, 231 | { 232 | "anonymous": false, 233 | "inputs": [ 234 | { 235 | "indexed": true, 236 | "name": "owner", 237 | "type": "address" 238 | }, 239 | { 240 | "indexed": true, 241 | "name": "approved", 242 | "type": "address" 243 | }, 244 | { 245 | "indexed": true, 246 | "name": "tokenId", 247 | "type": "uint256" 248 | } 249 | ], 250 | "name": "Approval", 251 | "type": "event" 252 | }, 253 | { 254 | "anonymous": false, 255 | "inputs": [ 256 | { 257 | "indexed": true, 258 | "name": "owner", 259 | "type": "address" 260 | }, 261 | { 262 | "indexed": true, 263 | "name": "operator", 264 | "type": "address" 265 | }, 266 | { 267 | "indexed": false, 268 | "name": "approved", 269 | "type": "bool" 270 | } 271 | ], 272 | "name": "ApprovalForAll", 273 | "type": "event" 274 | }, 275 | { 276 | "constant": true, 277 | "inputs": [], 278 | "name": "name", 279 | "outputs": [ 280 | { 281 | "name": "", 282 | "type": "string" 283 | } 284 | ], 285 | "payable": false, 286 | "stateMutability": "view", 287 | "type": "function" 288 | }, 289 | { 290 | "constant": true, 291 | "inputs": [], 292 | "name": "symbol", 293 | "outputs": [ 294 | { 295 | "name": "", 296 | "type": "string" 297 | } 298 | ], 299 | "payable": false, 300 | "stateMutability": "view", 301 | "type": "function" 302 | }, 303 | { 304 | "constant": true, 305 | "inputs": [ 306 | { 307 | "name": "tokenId", 308 | "type": "uint256" 309 | } 310 | ], 311 | "name": "tokenURI", 312 | "outputs": [ 313 | { 314 | "name": "", 315 | "type": "string" 316 | } 317 | ], 318 | "payable": false, 319 | "stateMutability": "view", 320 | "type": "function" 321 | } 322 | ], 323 | "bytecode": "0x", 324 | "deployedBytecode": "0x", 325 | "sourceMap": "", 326 | "deployedSourceMap": "", 327 | "source": "pragma solidity ^0.5.0;\n\nimport \"./IERC721.sol\";\n\n/**\n * @title ERC-721 Non-Fungible Token Standard, optional metadata extension\n * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md\n */\ncontract IERC721Metadata is IERC721 {\n function name() external view returns (string memory);\n function symbol() external view returns (string memory);\n function tokenURI(uint256 tokenId) external view returns (string memory);\n}\n", 328 | "sourcePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721Metadata.sol", 329 | "ast": { 330 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721Metadata.sol", 331 | "exportedSymbols": { 332 | "IERC721Metadata": [ 333 | 1600 334 | ] 335 | }, 336 | "id": 1601, 337 | "nodeType": "SourceUnit", 338 | "nodes": [ 339 | { 340 | "id": 1579, 341 | "literals": [ 342 | "solidity", 343 | "^", 344 | "0.5", 345 | ".0" 346 | ], 347 | "nodeType": "PragmaDirective", 348 | "src": "0:23:12" 349 | }, 350 | { 351 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721.sol", 352 | "file": "./IERC721.sol", 353 | "id": 1580, 354 | "nodeType": "ImportDirective", 355 | "scope": 1601, 356 | "sourceUnit": 1551, 357 | "src": "25:23:12", 358 | "symbolAliases": [], 359 | "unitAlias": "" 360 | }, 361 | { 362 | "baseContracts": [ 363 | { 364 | "arguments": null, 365 | "baseName": { 366 | "contractScope": null, 367 | "id": 1581, 368 | "name": "IERC721", 369 | "nodeType": "UserDefinedTypeName", 370 | "referencedDeclaration": 1550, 371 | "src": "234:7:12", 372 | "typeDescriptions": { 373 | "typeIdentifier": "t_contract$_IERC721_$1550", 374 | "typeString": "contract IERC721" 375 | } 376 | }, 377 | "id": 1582, 378 | "nodeType": "InheritanceSpecifier", 379 | "src": "234:7:12" 380 | } 381 | ], 382 | "contractDependencies": [ 383 | 250, 384 | 1550 385 | ], 386 | "contractKind": "contract", 387 | "documentation": "@title ERC-721 Non-Fungible Token Standard, optional metadata extension\n@dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md", 388 | "fullyImplemented": false, 389 | "id": 1600, 390 | "linearizedBaseContracts": [ 391 | 1600, 392 | 1550, 393 | 250 394 | ], 395 | "name": "IERC721Metadata", 396 | "nodeType": "ContractDefinition", 397 | "nodes": [ 398 | { 399 | "body": null, 400 | "documentation": null, 401 | "id": 1587, 402 | "implemented": false, 403 | "kind": "function", 404 | "modifiers": [], 405 | "name": "name", 406 | "nodeType": "FunctionDefinition", 407 | "parameters": { 408 | "id": 1583, 409 | "nodeType": "ParameterList", 410 | "parameters": [], 411 | "src": "261:2:12" 412 | }, 413 | "returnParameters": { 414 | "id": 1586, 415 | "nodeType": "ParameterList", 416 | "parameters": [ 417 | { 418 | "constant": false, 419 | "id": 1585, 420 | "name": "", 421 | "nodeType": "VariableDeclaration", 422 | "scope": 1587, 423 | "src": "287:13:12", 424 | "stateVariable": false, 425 | "storageLocation": "memory", 426 | "typeDescriptions": { 427 | "typeIdentifier": "t_string_memory_ptr", 428 | "typeString": "string" 429 | }, 430 | "typeName": { 431 | "id": 1584, 432 | "name": "string", 433 | "nodeType": "ElementaryTypeName", 434 | "src": "287:6:12", 435 | "typeDescriptions": { 436 | "typeIdentifier": "t_string_storage_ptr", 437 | "typeString": "string" 438 | } 439 | }, 440 | "value": null, 441 | "visibility": "internal" 442 | } 443 | ], 444 | "src": "286:15:12" 445 | }, 446 | "scope": 1600, 447 | "src": "248:54:12", 448 | "stateMutability": "view", 449 | "superFunction": null, 450 | "visibility": "external" 451 | }, 452 | { 453 | "body": null, 454 | "documentation": null, 455 | "id": 1592, 456 | "implemented": false, 457 | "kind": "function", 458 | "modifiers": [], 459 | "name": "symbol", 460 | "nodeType": "FunctionDefinition", 461 | "parameters": { 462 | "id": 1588, 463 | "nodeType": "ParameterList", 464 | "parameters": [], 465 | "src": "322:2:12" 466 | }, 467 | "returnParameters": { 468 | "id": 1591, 469 | "nodeType": "ParameterList", 470 | "parameters": [ 471 | { 472 | "constant": false, 473 | "id": 1590, 474 | "name": "", 475 | "nodeType": "VariableDeclaration", 476 | "scope": 1592, 477 | "src": "348:13:12", 478 | "stateVariable": false, 479 | "storageLocation": "memory", 480 | "typeDescriptions": { 481 | "typeIdentifier": "t_string_memory_ptr", 482 | "typeString": "string" 483 | }, 484 | "typeName": { 485 | "id": 1589, 486 | "name": "string", 487 | "nodeType": "ElementaryTypeName", 488 | "src": "348:6:12", 489 | "typeDescriptions": { 490 | "typeIdentifier": "t_string_storage_ptr", 491 | "typeString": "string" 492 | } 493 | }, 494 | "value": null, 495 | "visibility": "internal" 496 | } 497 | ], 498 | "src": "347:15:12" 499 | }, 500 | "scope": 1600, 501 | "src": "307:56:12", 502 | "stateMutability": "view", 503 | "superFunction": null, 504 | "visibility": "external" 505 | }, 506 | { 507 | "body": null, 508 | "documentation": null, 509 | "id": 1599, 510 | "implemented": false, 511 | "kind": "function", 512 | "modifiers": [], 513 | "name": "tokenURI", 514 | "nodeType": "FunctionDefinition", 515 | "parameters": { 516 | "id": 1595, 517 | "nodeType": "ParameterList", 518 | "parameters": [ 519 | { 520 | "constant": false, 521 | "id": 1594, 522 | "name": "tokenId", 523 | "nodeType": "VariableDeclaration", 524 | "scope": 1599, 525 | "src": "386:15:12", 526 | "stateVariable": false, 527 | "storageLocation": "default", 528 | "typeDescriptions": { 529 | "typeIdentifier": "t_uint256", 530 | "typeString": "uint256" 531 | }, 532 | "typeName": { 533 | "id": 1593, 534 | "name": "uint256", 535 | "nodeType": "ElementaryTypeName", 536 | "src": "386:7:12", 537 | "typeDescriptions": { 538 | "typeIdentifier": "t_uint256", 539 | "typeString": "uint256" 540 | } 541 | }, 542 | "value": null, 543 | "visibility": "internal" 544 | } 545 | ], 546 | "src": "385:17:12" 547 | }, 548 | "returnParameters": { 549 | "id": 1598, 550 | "nodeType": "ParameterList", 551 | "parameters": [ 552 | { 553 | "constant": false, 554 | "id": 1597, 555 | "name": "", 556 | "nodeType": "VariableDeclaration", 557 | "scope": 1599, 558 | "src": "426:13:12", 559 | "stateVariable": false, 560 | "storageLocation": "memory", 561 | "typeDescriptions": { 562 | "typeIdentifier": "t_string_memory_ptr", 563 | "typeString": "string" 564 | }, 565 | "typeName": { 566 | "id": 1596, 567 | "name": "string", 568 | "nodeType": "ElementaryTypeName", 569 | "src": "426:6:12", 570 | "typeDescriptions": { 571 | "typeIdentifier": "t_string_storage_ptr", 572 | "typeString": "string" 573 | } 574 | }, 575 | "value": null, 576 | "visibility": "internal" 577 | } 578 | ], 579 | "src": "425:15:12" 580 | }, 581 | "scope": 1600, 582 | "src": "368:73:12", 583 | "stateMutability": "view", 584 | "superFunction": null, 585 | "visibility": "external" 586 | } 587 | ], 588 | "scope": 1601, 589 | "src": "206:237:12" 590 | } 591 | ], 592 | "src": "0:444:12" 593 | }, 594 | "legacyAST": { 595 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721Metadata.sol", 596 | "exportedSymbols": { 597 | "IERC721Metadata": [ 598 | 1600 599 | ] 600 | }, 601 | "id": 1601, 602 | "nodeType": "SourceUnit", 603 | "nodes": [ 604 | { 605 | "id": 1579, 606 | "literals": [ 607 | "solidity", 608 | "^", 609 | "0.5", 610 | ".0" 611 | ], 612 | "nodeType": "PragmaDirective", 613 | "src": "0:23:12" 614 | }, 615 | { 616 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721.sol", 617 | "file": "./IERC721.sol", 618 | "id": 1580, 619 | "nodeType": "ImportDirective", 620 | "scope": 1601, 621 | "sourceUnit": 1551, 622 | "src": "25:23:12", 623 | "symbolAliases": [], 624 | "unitAlias": "" 625 | }, 626 | { 627 | "baseContracts": [ 628 | { 629 | "arguments": null, 630 | "baseName": { 631 | "contractScope": null, 632 | "id": 1581, 633 | "name": "IERC721", 634 | "nodeType": "UserDefinedTypeName", 635 | "referencedDeclaration": 1550, 636 | "src": "234:7:12", 637 | "typeDescriptions": { 638 | "typeIdentifier": "t_contract$_IERC721_$1550", 639 | "typeString": "contract IERC721" 640 | } 641 | }, 642 | "id": 1582, 643 | "nodeType": "InheritanceSpecifier", 644 | "src": "234:7:12" 645 | } 646 | ], 647 | "contractDependencies": [ 648 | 250, 649 | 1550 650 | ], 651 | "contractKind": "contract", 652 | "documentation": "@title ERC-721 Non-Fungible Token Standard, optional metadata extension\n@dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md", 653 | "fullyImplemented": false, 654 | "id": 1600, 655 | "linearizedBaseContracts": [ 656 | 1600, 657 | 1550, 658 | 250 659 | ], 660 | "name": "IERC721Metadata", 661 | "nodeType": "ContractDefinition", 662 | "nodes": [ 663 | { 664 | "body": null, 665 | "documentation": null, 666 | "id": 1587, 667 | "implemented": false, 668 | "kind": "function", 669 | "modifiers": [], 670 | "name": "name", 671 | "nodeType": "FunctionDefinition", 672 | "parameters": { 673 | "id": 1583, 674 | "nodeType": "ParameterList", 675 | "parameters": [], 676 | "src": "261:2:12" 677 | }, 678 | "returnParameters": { 679 | "id": 1586, 680 | "nodeType": "ParameterList", 681 | "parameters": [ 682 | { 683 | "constant": false, 684 | "id": 1585, 685 | "name": "", 686 | "nodeType": "VariableDeclaration", 687 | "scope": 1587, 688 | "src": "287:13:12", 689 | "stateVariable": false, 690 | "storageLocation": "memory", 691 | "typeDescriptions": { 692 | "typeIdentifier": "t_string_memory_ptr", 693 | "typeString": "string" 694 | }, 695 | "typeName": { 696 | "id": 1584, 697 | "name": "string", 698 | "nodeType": "ElementaryTypeName", 699 | "src": "287:6:12", 700 | "typeDescriptions": { 701 | "typeIdentifier": "t_string_storage_ptr", 702 | "typeString": "string" 703 | } 704 | }, 705 | "value": null, 706 | "visibility": "internal" 707 | } 708 | ], 709 | "src": "286:15:12" 710 | }, 711 | "scope": 1600, 712 | "src": "248:54:12", 713 | "stateMutability": "view", 714 | "superFunction": null, 715 | "visibility": "external" 716 | }, 717 | { 718 | "body": null, 719 | "documentation": null, 720 | "id": 1592, 721 | "implemented": false, 722 | "kind": "function", 723 | "modifiers": [], 724 | "name": "symbol", 725 | "nodeType": "FunctionDefinition", 726 | "parameters": { 727 | "id": 1588, 728 | "nodeType": "ParameterList", 729 | "parameters": [], 730 | "src": "322:2:12" 731 | }, 732 | "returnParameters": { 733 | "id": 1591, 734 | "nodeType": "ParameterList", 735 | "parameters": [ 736 | { 737 | "constant": false, 738 | "id": 1590, 739 | "name": "", 740 | "nodeType": "VariableDeclaration", 741 | "scope": 1592, 742 | "src": "348:13:12", 743 | "stateVariable": false, 744 | "storageLocation": "memory", 745 | "typeDescriptions": { 746 | "typeIdentifier": "t_string_memory_ptr", 747 | "typeString": "string" 748 | }, 749 | "typeName": { 750 | "id": 1589, 751 | "name": "string", 752 | "nodeType": "ElementaryTypeName", 753 | "src": "348:6:12", 754 | "typeDescriptions": { 755 | "typeIdentifier": "t_string_storage_ptr", 756 | "typeString": "string" 757 | } 758 | }, 759 | "value": null, 760 | "visibility": "internal" 761 | } 762 | ], 763 | "src": "347:15:12" 764 | }, 765 | "scope": 1600, 766 | "src": "307:56:12", 767 | "stateMutability": "view", 768 | "superFunction": null, 769 | "visibility": "external" 770 | }, 771 | { 772 | "body": null, 773 | "documentation": null, 774 | "id": 1599, 775 | "implemented": false, 776 | "kind": "function", 777 | "modifiers": [], 778 | "name": "tokenURI", 779 | "nodeType": "FunctionDefinition", 780 | "parameters": { 781 | "id": 1595, 782 | "nodeType": "ParameterList", 783 | "parameters": [ 784 | { 785 | "constant": false, 786 | "id": 1594, 787 | "name": "tokenId", 788 | "nodeType": "VariableDeclaration", 789 | "scope": 1599, 790 | "src": "386:15:12", 791 | "stateVariable": false, 792 | "storageLocation": "default", 793 | "typeDescriptions": { 794 | "typeIdentifier": "t_uint256", 795 | "typeString": "uint256" 796 | }, 797 | "typeName": { 798 | "id": 1593, 799 | "name": "uint256", 800 | "nodeType": "ElementaryTypeName", 801 | "src": "386:7:12", 802 | "typeDescriptions": { 803 | "typeIdentifier": "t_uint256", 804 | "typeString": "uint256" 805 | } 806 | }, 807 | "value": null, 808 | "visibility": "internal" 809 | } 810 | ], 811 | "src": "385:17:12" 812 | }, 813 | "returnParameters": { 814 | "id": 1598, 815 | "nodeType": "ParameterList", 816 | "parameters": [ 817 | { 818 | "constant": false, 819 | "id": 1597, 820 | "name": "", 821 | "nodeType": "VariableDeclaration", 822 | "scope": 1599, 823 | "src": "426:13:12", 824 | "stateVariable": false, 825 | "storageLocation": "memory", 826 | "typeDescriptions": { 827 | "typeIdentifier": "t_string_memory_ptr", 828 | "typeString": "string" 829 | }, 830 | "typeName": { 831 | "id": 1596, 832 | "name": "string", 833 | "nodeType": "ElementaryTypeName", 834 | "src": "426:6:12", 835 | "typeDescriptions": { 836 | "typeIdentifier": "t_string_storage_ptr", 837 | "typeString": "string" 838 | } 839 | }, 840 | "value": null, 841 | "visibility": "internal" 842 | } 843 | ], 844 | "src": "425:15:12" 845 | }, 846 | "scope": 1600, 847 | "src": "368:73:12", 848 | "stateMutability": "view", 849 | "superFunction": null, 850 | "visibility": "external" 851 | } 852 | ], 853 | "scope": 1601, 854 | "src": "206:237:12" 855 | } 856 | ], 857 | "src": "0:444:12" 858 | }, 859 | "compiler": { 860 | "name": "solc", 861 | "version": "0.5.0+commit.1d4f565a.Emscripten.clang" 862 | }, 863 | "networks": {}, 864 | "schemaVersion": "3.0.1", 865 | "updatedAt": "2019-01-08T23:23:58.049Z", 866 | "devdoc": { 867 | "details": "See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md", 868 | "methods": { 869 | "supportsInterface(bytes4)": { 870 | "details": "Interface identification is specified in ERC-165. This function uses less than 30,000 gas.", 871 | "params": { 872 | "interfaceId": "The interface identifier, as specified in ERC-165" 873 | } 874 | } 875 | }, 876 | "title": "ERC-721 Non-Fungible Token Standard, optional metadata extension" 877 | }, 878 | "userdoc": { 879 | "methods": { 880 | "supportsInterface(bytes4)": { 881 | "notice": "Query if a contract implements an interface" 882 | } 883 | } 884 | } 885 | } -------------------------------------------------------------------------------- /build/contracts/IERC721Receiver.json: -------------------------------------------------------------------------------- 1 | { 2 | "contractName": "IERC721Receiver", 3 | "abi": [ 4 | { 5 | "constant": false, 6 | "inputs": [ 7 | { 8 | "name": "operator", 9 | "type": "address" 10 | }, 11 | { 12 | "name": "from", 13 | "type": "address" 14 | }, 15 | { 16 | "name": "tokenId", 17 | "type": "uint256" 18 | }, 19 | { 20 | "name": "data", 21 | "type": "bytes" 22 | } 23 | ], 24 | "name": "onERC721Received", 25 | "outputs": [ 26 | { 27 | "name": "", 28 | "type": "bytes4" 29 | } 30 | ], 31 | "payable": false, 32 | "stateMutability": "nonpayable", 33 | "type": "function" 34 | } 35 | ], 36 | "bytecode": "0x", 37 | "deployedBytecode": "0x", 38 | "sourceMap": "", 39 | "deployedSourceMap": "", 40 | "source": "pragma solidity ^0.5.0;\n\n/**\n * @title ERC721 token receiver interface\n * @dev Interface for any contract that wants to support safeTransfers\n * from ERC721 asset contracts.\n */\ncontract IERC721Receiver {\n /**\n * @notice Handle the receipt of an NFT\n * @dev The ERC721 smart contract calls this function on the recipient\n * after a `safeTransfer`. This function MUST return the function selector,\n * otherwise the caller will revert the transaction. The selector to be\n * returned can be obtained as `this.onERC721Received.selector`. This\n * function MAY throw to revert and reject the transfer.\n * Note: the ERC721 contract address is always the message sender.\n * @param operator The address which called `safeTransferFrom` function\n * @param from The address which previously owned the token\n * @param tokenId The NFT identifier which is being transferred\n * @param data Additional data with no specified format\n * @return `bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\"))`\n */\n function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data)\n public returns (bytes4);\n}\n", 41 | "sourcePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721Receiver.sol", 42 | "ast": { 43 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721Receiver.sol", 44 | "exportedSymbols": { 45 | "IERC721Receiver": [ 46 | 1616 47 | ] 48 | }, 49 | "id": 1617, 50 | "nodeType": "SourceUnit", 51 | "nodes": [ 52 | { 53 | "id": 1602, 54 | "literals": [ 55 | "solidity", 56 | "^", 57 | "0.5", 58 | ".0" 59 | ], 60 | "nodeType": "PragmaDirective", 61 | "src": "0:23:13" 62 | }, 63 | { 64 | "baseContracts": [], 65 | "contractDependencies": [], 66 | "contractKind": "contract", 67 | "documentation": "@title ERC721 token receiver interface\n@dev Interface for any contract that wants to support safeTransfers\nfrom ERC721 asset contracts.", 68 | "fullyImplemented": false, 69 | "id": 1616, 70 | "linearizedBaseContracts": [ 71 | 1616 72 | ], 73 | "name": "IERC721Receiver", 74 | "nodeType": "ContractDefinition", 75 | "nodes": [ 76 | { 77 | "body": null, 78 | "documentation": "@notice Handle the receipt of an NFT\n@dev The ERC721 smart contract calls this function on the recipient\nafter a `safeTransfer`. This function MUST return the function selector,\notherwise the caller will revert the transaction. The selector to be\nreturned can be obtained as `this.onERC721Received.selector`. This\nfunction MAY throw to revert and reject the transfer.\nNote: the ERC721 contract address is always the message sender.\n@param operator The address which called `safeTransferFrom` function\n@param from The address which previously owned the token\n@param tokenId The NFT identifier which is being transferred\n@param data Additional data with no specified format\n@return `bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\"))`", 79 | "id": 1615, 80 | "implemented": false, 81 | "kind": "function", 82 | "modifiers": [], 83 | "name": "onERC721Received", 84 | "nodeType": "FunctionDefinition", 85 | "parameters": { 86 | "id": 1611, 87 | "nodeType": "ParameterList", 88 | "parameters": [ 89 | { 90 | "constant": false, 91 | "id": 1604, 92 | "name": "operator", 93 | "nodeType": "VariableDeclaration", 94 | "scope": 1615, 95 | "src": "1086:16:13", 96 | "stateVariable": false, 97 | "storageLocation": "default", 98 | "typeDescriptions": { 99 | "typeIdentifier": "t_address", 100 | "typeString": "address" 101 | }, 102 | "typeName": { 103 | "id": 1603, 104 | "name": "address", 105 | "nodeType": "ElementaryTypeName", 106 | "src": "1086:7:13", 107 | "stateMutability": "nonpayable", 108 | "typeDescriptions": { 109 | "typeIdentifier": "t_address", 110 | "typeString": "address" 111 | } 112 | }, 113 | "value": null, 114 | "visibility": "internal" 115 | }, 116 | { 117 | "constant": false, 118 | "id": 1606, 119 | "name": "from", 120 | "nodeType": "VariableDeclaration", 121 | "scope": 1615, 122 | "src": "1104:12:13", 123 | "stateVariable": false, 124 | "storageLocation": "default", 125 | "typeDescriptions": { 126 | "typeIdentifier": "t_address", 127 | "typeString": "address" 128 | }, 129 | "typeName": { 130 | "id": 1605, 131 | "name": "address", 132 | "nodeType": "ElementaryTypeName", 133 | "src": "1104:7:13", 134 | "stateMutability": "nonpayable", 135 | "typeDescriptions": { 136 | "typeIdentifier": "t_address", 137 | "typeString": "address" 138 | } 139 | }, 140 | "value": null, 141 | "visibility": "internal" 142 | }, 143 | { 144 | "constant": false, 145 | "id": 1608, 146 | "name": "tokenId", 147 | "nodeType": "VariableDeclaration", 148 | "scope": 1615, 149 | "src": "1118:15:13", 150 | "stateVariable": false, 151 | "storageLocation": "default", 152 | "typeDescriptions": { 153 | "typeIdentifier": "t_uint256", 154 | "typeString": "uint256" 155 | }, 156 | "typeName": { 157 | "id": 1607, 158 | "name": "uint256", 159 | "nodeType": "ElementaryTypeName", 160 | "src": "1118:7:13", 161 | "typeDescriptions": { 162 | "typeIdentifier": "t_uint256", 163 | "typeString": "uint256" 164 | } 165 | }, 166 | "value": null, 167 | "visibility": "internal" 168 | }, 169 | { 170 | "constant": false, 171 | "id": 1610, 172 | "name": "data", 173 | "nodeType": "VariableDeclaration", 174 | "scope": 1615, 175 | "src": "1135:17:13", 176 | "stateVariable": false, 177 | "storageLocation": "memory", 178 | "typeDescriptions": { 179 | "typeIdentifier": "t_bytes_memory_ptr", 180 | "typeString": "bytes" 181 | }, 182 | "typeName": { 183 | "id": 1609, 184 | "name": "bytes", 185 | "nodeType": "ElementaryTypeName", 186 | "src": "1135:5:13", 187 | "typeDescriptions": { 188 | "typeIdentifier": "t_bytes_storage_ptr", 189 | "typeString": "bytes" 190 | } 191 | }, 192 | "value": null, 193 | "visibility": "internal" 194 | } 195 | ], 196 | "src": "1085:68:13" 197 | }, 198 | "returnParameters": { 199 | "id": 1614, 200 | "nodeType": "ParameterList", 201 | "parameters": [ 202 | { 203 | "constant": false, 204 | "id": 1613, 205 | "name": "", 206 | "nodeType": "VariableDeclaration", 207 | "scope": 1615, 208 | "src": "1174:6:13", 209 | "stateVariable": false, 210 | "storageLocation": "default", 211 | "typeDescriptions": { 212 | "typeIdentifier": "t_bytes4", 213 | "typeString": "bytes4" 214 | }, 215 | "typeName": { 216 | "id": 1612, 217 | "name": "bytes4", 218 | "nodeType": "ElementaryTypeName", 219 | "src": "1174:6:13", 220 | "typeDescriptions": { 221 | "typeIdentifier": "t_bytes4", 222 | "typeString": "bytes4" 223 | } 224 | }, 225 | "value": null, 226 | "visibility": "internal" 227 | } 228 | ], 229 | "src": "1173:8:13" 230 | }, 231 | "scope": 1616, 232 | "src": "1060:122:13", 233 | "stateMutability": "nonpayable", 234 | "superFunction": null, 235 | "visibility": "public" 236 | } 237 | ], 238 | "scope": 1617, 239 | "src": "178:1006:13" 240 | } 241 | ], 242 | "src": "0:1185:13" 243 | }, 244 | "legacyAST": { 245 | "absolutePath": "openzeppelin-solidity/contracts/token/ERC721/IERC721Receiver.sol", 246 | "exportedSymbols": { 247 | "IERC721Receiver": [ 248 | 1616 249 | ] 250 | }, 251 | "id": 1617, 252 | "nodeType": "SourceUnit", 253 | "nodes": [ 254 | { 255 | "id": 1602, 256 | "literals": [ 257 | "solidity", 258 | "^", 259 | "0.5", 260 | ".0" 261 | ], 262 | "nodeType": "PragmaDirective", 263 | "src": "0:23:13" 264 | }, 265 | { 266 | "baseContracts": [], 267 | "contractDependencies": [], 268 | "contractKind": "contract", 269 | "documentation": "@title ERC721 token receiver interface\n@dev Interface for any contract that wants to support safeTransfers\nfrom ERC721 asset contracts.", 270 | "fullyImplemented": false, 271 | "id": 1616, 272 | "linearizedBaseContracts": [ 273 | 1616 274 | ], 275 | "name": "IERC721Receiver", 276 | "nodeType": "ContractDefinition", 277 | "nodes": [ 278 | { 279 | "body": null, 280 | "documentation": "@notice Handle the receipt of an NFT\n@dev The ERC721 smart contract calls this function on the recipient\nafter a `safeTransfer`. This function MUST return the function selector,\notherwise the caller will revert the transaction. The selector to be\nreturned can be obtained as `this.onERC721Received.selector`. This\nfunction MAY throw to revert and reject the transfer.\nNote: the ERC721 contract address is always the message sender.\n@param operator The address which called `safeTransferFrom` function\n@param from The address which previously owned the token\n@param tokenId The NFT identifier which is being transferred\n@param data Additional data with no specified format\n@return `bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\"))`", 281 | "id": 1615, 282 | "implemented": false, 283 | "kind": "function", 284 | "modifiers": [], 285 | "name": "onERC721Received", 286 | "nodeType": "FunctionDefinition", 287 | "parameters": { 288 | "id": 1611, 289 | "nodeType": "ParameterList", 290 | "parameters": [ 291 | { 292 | "constant": false, 293 | "id": 1604, 294 | "name": "operator", 295 | "nodeType": "VariableDeclaration", 296 | "scope": 1615, 297 | "src": "1086:16:13", 298 | "stateVariable": false, 299 | "storageLocation": "default", 300 | "typeDescriptions": { 301 | "typeIdentifier": "t_address", 302 | "typeString": "address" 303 | }, 304 | "typeName": { 305 | "id": 1603, 306 | "name": "address", 307 | "nodeType": "ElementaryTypeName", 308 | "src": "1086:7:13", 309 | "stateMutability": "nonpayable", 310 | "typeDescriptions": { 311 | "typeIdentifier": "t_address", 312 | "typeString": "address" 313 | } 314 | }, 315 | "value": null, 316 | "visibility": "internal" 317 | }, 318 | { 319 | "constant": false, 320 | "id": 1606, 321 | "name": "from", 322 | "nodeType": "VariableDeclaration", 323 | "scope": 1615, 324 | "src": "1104:12:13", 325 | "stateVariable": false, 326 | "storageLocation": "default", 327 | "typeDescriptions": { 328 | "typeIdentifier": "t_address", 329 | "typeString": "address" 330 | }, 331 | "typeName": { 332 | "id": 1605, 333 | "name": "address", 334 | "nodeType": "ElementaryTypeName", 335 | "src": "1104:7:13", 336 | "stateMutability": "nonpayable", 337 | "typeDescriptions": { 338 | "typeIdentifier": "t_address", 339 | "typeString": "address" 340 | } 341 | }, 342 | "value": null, 343 | "visibility": "internal" 344 | }, 345 | { 346 | "constant": false, 347 | "id": 1608, 348 | "name": "tokenId", 349 | "nodeType": "VariableDeclaration", 350 | "scope": 1615, 351 | "src": "1118:15:13", 352 | "stateVariable": false, 353 | "storageLocation": "default", 354 | "typeDescriptions": { 355 | "typeIdentifier": "t_uint256", 356 | "typeString": "uint256" 357 | }, 358 | "typeName": { 359 | "id": 1607, 360 | "name": "uint256", 361 | "nodeType": "ElementaryTypeName", 362 | "src": "1118:7:13", 363 | "typeDescriptions": { 364 | "typeIdentifier": "t_uint256", 365 | "typeString": "uint256" 366 | } 367 | }, 368 | "value": null, 369 | "visibility": "internal" 370 | }, 371 | { 372 | "constant": false, 373 | "id": 1610, 374 | "name": "data", 375 | "nodeType": "VariableDeclaration", 376 | "scope": 1615, 377 | "src": "1135:17:13", 378 | "stateVariable": false, 379 | "storageLocation": "memory", 380 | "typeDescriptions": { 381 | "typeIdentifier": "t_bytes_memory_ptr", 382 | "typeString": "bytes" 383 | }, 384 | "typeName": { 385 | "id": 1609, 386 | "name": "bytes", 387 | "nodeType": "ElementaryTypeName", 388 | "src": "1135:5:13", 389 | "typeDescriptions": { 390 | "typeIdentifier": "t_bytes_storage_ptr", 391 | "typeString": "bytes" 392 | } 393 | }, 394 | "value": null, 395 | "visibility": "internal" 396 | } 397 | ], 398 | "src": "1085:68:13" 399 | }, 400 | "returnParameters": { 401 | "id": 1614, 402 | "nodeType": "ParameterList", 403 | "parameters": [ 404 | { 405 | "constant": false, 406 | "id": 1613, 407 | "name": "", 408 | "nodeType": "VariableDeclaration", 409 | "scope": 1615, 410 | "src": "1174:6:13", 411 | "stateVariable": false, 412 | "storageLocation": "default", 413 | "typeDescriptions": { 414 | "typeIdentifier": "t_bytes4", 415 | "typeString": "bytes4" 416 | }, 417 | "typeName": { 418 | "id": 1612, 419 | "name": "bytes4", 420 | "nodeType": "ElementaryTypeName", 421 | "src": "1174:6:13", 422 | "typeDescriptions": { 423 | "typeIdentifier": "t_bytes4", 424 | "typeString": "bytes4" 425 | } 426 | }, 427 | "value": null, 428 | "visibility": "internal" 429 | } 430 | ], 431 | "src": "1173:8:13" 432 | }, 433 | "scope": 1616, 434 | "src": "1060:122:13", 435 | "stateMutability": "nonpayable", 436 | "superFunction": null, 437 | "visibility": "public" 438 | } 439 | ], 440 | "scope": 1617, 441 | "src": "178:1006:13" 442 | } 443 | ], 444 | "src": "0:1185:13" 445 | }, 446 | "compiler": { 447 | "name": "solc", 448 | "version": "0.5.0+commit.1d4f565a.Emscripten.clang" 449 | }, 450 | "networks": {}, 451 | "schemaVersion": "3.0.1", 452 | "updatedAt": "2019-01-08T23:23:58.049Z", 453 | "devdoc": { 454 | "details": "Interface for any contract that wants to support safeTransfers from ERC721 asset contracts.", 455 | "methods": { 456 | "onERC721Received(address,address,uint256,bytes)": { 457 | "details": "The ERC721 smart contract calls this function on the recipient after a `safeTransfer`. This function MUST return the function selector, otherwise the caller will revert the transaction. The selector to be returned can be obtained as `this.onERC721Received.selector`. This function MAY throw to revert and reject the transfer. Note: the ERC721 contract address is always the message sender.", 458 | "params": { 459 | "data": "Additional data with no specified format", 460 | "from": "The address which previously owned the token", 461 | "operator": "The address which called `safeTransferFrom` function", 462 | "tokenId": "The NFT identifier which is being transferred" 463 | }, 464 | "return": "`bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\"))`" 465 | } 466 | }, 467 | "title": "ERC721 token receiver interface" 468 | }, 469 | "userdoc": { 470 | "methods": { 471 | "onERC721Received(address,address,uint256,bytes)": { 472 | "notice": "Handle the receipt of an NFT" 473 | } 474 | } 475 | } 476 | } -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Stripe Demo - Client App 2 | 3 | The client app is responsible for providing the user interface, as well as collecting payment information and submitting that information to the server for processing. Your tokens are loaded directly from the token contract on the blockchain. 4 | 5 | ## Create your credentials 6 | 7 | You'll need an app id to actually run this demo. Visit https://developer.bitski.com/ and click "Create App". You can enter anything for the name and leave the url blank for now. 8 | 9 | Once your app is created, view your app details, then go to the OAuth settings, and under Redirect Urls, add the following: 10 | 11 | http://localhost:3000/callback.html 12 | 13 | You'll also need a Stripe API Key. You can get one by signing up for a Stripe account at https://stripe.com, and then navigating to Developers > API Keys. 14 | 15 | Finally, make sure you've already deployed your contracts by following the README in the root directory. 16 | 17 | ## Configure the app 18 | 19 | Create a `.env` file in this directory (client), then fill in the following: 20 | 21 | - BITSKI_CLIENT_ID: Your Bitski app id 22 | - STRIPE_API_KEY: Your Stripe API Key (the public key) 23 | - CONTRACT_ADDRESS: The address your token contract is deployed at (LimitedMintableNonFungibleToken) 24 | - NETWORK_NAME: The network to use (rinkeby | mainnet | kovan) 25 | - BITSKI_REDIRECT_URL: The URL to your callback.html file (/public/callback.html) 26 | 27 | It should look something like this: 28 | 29 | ``` 30 | BITSKI_CLIENT_ID=YOUR-ID 31 | BITSKI_REDIRECT_URL=http://localhost:3000/public/callback.html 32 | NETWORK_NAME=rinkeby 33 | STRIPE_API_KEY=YOUR-KEY 34 | CONTRACT_ADDRESS=YOUR-ADDRESS 35 | ``` 36 | 37 | You can also pass these in as regular environment variables at build time if you would prefer. 38 | 39 | ## Running Locally 40 | 41 | Now you should be ready to run the demo app locally. First, install the dependencies. 42 | 43 | ```bash 44 | npm install 45 | ``` 46 | 47 | Next, run the dev server: 48 | 49 | ```bash 50 | npm run dev 51 | ``` 52 | 53 | Then browse to [http://localhost:3000](http://localhost:3000) to interact with the app. As you make changes, the page will automatically be reloaded. 54 | 55 | ## Building for production 56 | 57 | To run in production mode, first build the app: 58 | 59 | ``` 60 | npm run build 61 | ``` 62 | 63 | Then, you can start the web server 64 | 65 | ``` 66 | npm start 67 | ``` 68 | 69 | ## Game 70 | 71 | The front-end of our app is a game using the Phaser framework. The code is located in [app](app/) and the assets are located in ```assets```. 72 | -------------------------------------------------------------------------------- /client/app/controllers/Game.js: -------------------------------------------------------------------------------- 1 | import 'phaser'; 2 | 3 | import CrewScene from '../scenes/CrewScene.js'; 4 | import BootScene from '../scenes/BootScene.js'; 5 | import UnitScene from '../scenes/UnitScene.js'; 6 | import ReceiptScene from '../scenes/ReceiptScene.js'; 7 | import TransactionScene from '../scenes/TransactionScene.js'; 8 | import AppWalletService from '../services/AppWalletService.js'; 9 | import StripeService from '../services/StripeService.js'; 10 | import TokenService from '../services/TokenService.js'; 11 | import Web3 from 'web3'; 12 | import { TransferModal } from '../views/Transfer.js'; 13 | 14 | export default class Game { 15 | 16 | constructor(parent, provider) { 17 | this.parent = parent; 18 | this.web3 = new Web3(provider); 19 | this.stripe = new StripeService(STRIPE_API_KEY); 20 | this.appWallet = new AppWalletService(APP_WALLET_URL || 'http://localhost:4200'); 21 | 22 | this.setAccount().then(() => { 23 | this.tokenService = new TokenService(this.web3, CONTRACT_ADDRESS, this.currentAccount); 24 | this.loadGame(); 25 | }); 26 | } 27 | 28 | setAccount() { 29 | return this.web3.eth.getAccounts().then(accounts => { 30 | this.currentAccount = accounts[0]; 31 | return accounts[0]; 32 | }).catch(error => { 33 | console.error(error); 34 | }); 35 | } 36 | 37 | getBalance() { 38 | return this.web3.eth.getBalance(this.currentAccount); 39 | } 40 | 41 | purchaseToken() { 42 | return this.stripe.showCheckoutForm('1 x Bitski Guy', 100).then(token => { 43 | return this.processTransaction(token); 44 | }); 45 | } 46 | 47 | processTransaction(token) { 48 | return this.appWallet.processPurchase(token, this.currentAccount); 49 | } 50 | 51 | showTokenInfo(tokenId) { 52 | window.open(`https://rinkeby.opensea.io/assets/${this.tokenService.address}/${tokenId}`, '_blank'); 53 | } 54 | 55 | showTransactionInfo(transactionHash) { 56 | window.open(`https://rinkeby.etherscan.io/tx/${transactionHash}`, '_blank'); 57 | } 58 | 59 | showTransferUI(token) { 60 | this.transferModal = new TransferModal(token, (recipient) => { 61 | const method = this.tokenService.transfer(token.id, recipient); 62 | this.gameEngine.scene.stop('unit'); 63 | this.gameEngine.scene.start('transaction', { owner: this, method: method, completion: () => { 64 | this.gameEngine.scene.stop('transaction'); 65 | this.gameEngine.scene.start('boot'); 66 | }}); 67 | this.transferModal = undefined; 68 | }); 69 | this.transferModal.show(); 70 | } 71 | 72 | logOut() { 73 | this.parent.logOut(); 74 | } 75 | 76 | destroy() { 77 | this.gameEngine.destroy(true); 78 | } 79 | 80 | loadGame() { 81 | 82 | const bootScene = new BootScene(this); 83 | 84 | const gameConfig = { 85 | type: Phaser.AUTO, 86 | scale: { 87 | parent: 'game', 88 | mode: Phaser.Scale.FIT, 89 | autoCenter: Phaser.Scale.CENTER_BOTH, 90 | width: 1200, 91 | height: 1200 92 | }, 93 | physics: { 94 | default: 'arcade', 95 | arcade: { 96 | gravity: { 97 | y: 200 98 | } 99 | } 100 | }, 101 | scene: [bootScene, CrewScene, UnitScene, TransactionScene, ReceiptScene], 102 | }; 103 | 104 | const game = new Phaser.Game(gameConfig); 105 | this.gameEngine = game; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /client/app/controllers/Index.js: -------------------------------------------------------------------------------- 1 | import { Bitski, AuthenticationStatus } from 'bitski'; 2 | import Web3 from 'web3'; 3 | import Game from './Game.js'; 4 | import { LoggedInView } from '../views/LoggedIn.js'; 5 | import { LoggedOutView } from '../views/LoggedOut.js'; 6 | 7 | const LOGGED_OUT_SELECTOR = "#signed-out"; 8 | const LOGGED_IN_SELECTOR = "#game"; 9 | 10 | export class Index { 11 | 12 | constructor() { 13 | this.bitski = new Bitski(BITSKI_CLIENT_ID, BITSKI_REDIRECT_URL); 14 | this.loggedInView = new LoggedInView(LOGGED_IN_SELECTOR); 15 | this.loggedOutView = new LoggedOutView(LOGGED_OUT_SELECTOR, this.bitski, (provider) => { 16 | this.startGame(provider); 17 | }); 18 | } 19 | 20 | start() { 21 | this.checkAuthStatus(); 22 | } 23 | 24 | checkAuthStatus() { 25 | if (this.bitski.authStatus !== AuthenticationStatus.NotConnected) { 26 | this.startGame(this.bitski.getProvider(BITSKI_PROVIDER_ID)); 27 | } else { 28 | this.showLogin(); 29 | } 30 | } 31 | 32 | showLogin() { 33 | this.loggedInView.hide(); 34 | this.loggedOutView.show(); 35 | } 36 | 37 | showApp() { 38 | this.loggedOutView.hide(); 39 | this.loggedInView.show(); 40 | } 41 | 42 | logOut() { 43 | this.bitski.signOut().then(() => { 44 | this.showLogin(); 45 | }).catch((error) => { 46 | this.showLogin(); 47 | console.error(error); 48 | }); 49 | } 50 | 51 | verifyNetwork() { 52 | return this.web3.eth.net.getId().then(netId => { 53 | if (netId !== EXPECTED_NETWORK_ID) { 54 | throw new Error(`Please set your network to ${EXPECTED_NETWORK_NAME}`); 55 | } 56 | return true; 57 | }); 58 | } 59 | 60 | startGame(provider) { 61 | this.web3 = new Web3(provider); 62 | this.verifyNetwork().then(() => { 63 | this.showApp(); 64 | this.game = new Game(this, provider); 65 | }).catch(error => { 66 | console.error(error); 67 | alert(error.message); 68 | }); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /client/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example NFT with Stripe 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 30 |
31 |
32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/app/index.js: -------------------------------------------------------------------------------- 1 | import Raven from 'raven-js'; 2 | import { Index } from './controllers/Index'; 3 | 4 | if (SENTRY_DSN) { 5 | Raven.config(SENTRY_DSN).install(); 6 | } 7 | 8 | const controller = new Index(); 9 | 10 | // Load webfonts before rendering app 11 | WebFont.load({ 12 | google: { 13 | families: ['Acme'] 14 | }, 15 | active: () => { 16 | controller.start(); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /client/app/scenes/BootScene.js: -------------------------------------------------------------------------------- 1 | import { Scene } from 'phaser'; 2 | import styles from '../utils/styles'; 3 | 4 | const labelStyle = { 5 | fontSize: '64px', 6 | fontFamily: 'Acme', 7 | color: '#ffffff', 8 | align: 'center', 9 | backgroundColor: '#2DAA58' 10 | }; 11 | 12 | const labelConfig = { 13 | x: 600, 14 | y: 0, 15 | origin: { x: 0.5, y: 0 }, 16 | padding: 20, 17 | text: 'Loading....', 18 | style: styles.title 19 | }; 20 | 21 | const whatsHappeningStyle = { 22 | backgroundColor: '#333333', 23 | font: '32px Acme', 24 | fill: 'white', 25 | wordWrap: { width: 1200 } 26 | } 27 | 28 | export default class BootScene extends Scene { 29 | constructor(owner) { 30 | super({ key: 'boot', active: true }); 31 | this.owner = owner; 32 | } 33 | 34 | create() { 35 | this.make.text({ 36 | x: 0, 37 | y: 1200, 38 | padding: 20, 39 | origin: { x: 0, y: 1 }, 40 | text: "Whats Happening?\n\nWe are querying the ethereum network. If this takes a while something might be broken...", 41 | style: styles.explanation 42 | }); 43 | this.make.text(labelConfig); 44 | 45 | this.owner.tokenService.list().then((tokens) => { 46 | this.scene.start('crew', { owner: this.owner, tokens: tokens}); 47 | }).catch(console.alert); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /client/app/scenes/CrewScene.js: -------------------------------------------------------------------------------- 1 | import { Scene } from 'phaser'; 2 | import styles from '../utils/styles'; 3 | 4 | const characterPositions = [ 5 | [200, 340], 6 | [520, 300], 7 | [840, 380], 8 | [360, 620], 9 | [680, 620], 10 | ]; 11 | 12 | export default class CrewScene extends Scene { 13 | constructor() { 14 | super({ key: 'crew', active: false }); 15 | this.tokens = []; 16 | } 17 | 18 | preload() { 19 | this.load.image('character-1', 'assets/character-1.png'); 20 | this.load.image('character-2', 'assets/character-2.png'); 21 | this.load.image('character-3', 'assets/character-3.png'); 22 | this.load.image('character-4', 'assets/character-4.png'); 23 | this.load.image('character-5', 'assets/character-5.png'); 24 | } 25 | 26 | buyToken() { 27 | this.owner.purchaseToken().then(response => { 28 | this.showPurchasedToken(response.token, response.transactionHash); 29 | }).catch(error => { 30 | console.error(error); 31 | }); 32 | } 33 | 34 | showPurchasedToken(token, transactionHash) { 35 | this.owner.tokenService.unsubscribe(); 36 | this.scene.stop('crew'); 37 | this.scene.start('receipt', { owner: this.owner, token: token, transactionHash: transactionHash }); 38 | } 39 | 40 | showTokenDetails(token) { 41 | this.owner.tokenService.unsubscribe(); 42 | this.scene.stop('crew'); 43 | this.scene.start('unit', { owner: this.owner, token: token }); 44 | } 45 | 46 | drawToken(token, index) { 47 | let character = token.imageId; 48 | let characterPosition = characterPositions[index]; 49 | let characterImage = this.physics.add.image(characterPosition[0], characterPosition[1], `character-${character}`); 50 | characterImage.setScale(1.2); 51 | characterImage.setOrigin(0,0); 52 | let velocityX = Math.random() * (100 - (-100)) + (-100); 53 | let velocityY = Math.random() * (300 - (-300)) + (-300); 54 | characterImage.setVelocity(velocityX, velocityY); 55 | characterImage.setBounce(1, 1); 56 | characterImage.setGravityY(200); 57 | characterImage.setCollideWorldBounds(true); 58 | 59 | characterImage.setInteractive({ useHandCursor: true }); 60 | characterImage.on('pointerup', () => { 61 | this.showTokenDetails(token); 62 | }); 63 | this.characters.push(characterImage); 64 | } 65 | 66 | configureTokens() { 67 | let totalTokens = this.tokens.length; 68 | 69 | this.characters.forEach(image => { 70 | image.destroy(); 71 | }); 72 | 73 | this.characters = []; 74 | 75 | this.tokens.forEach((token, i) => { 76 | this.drawToken(token, i); 77 | }); 78 | 79 | var title = '...' 80 | 81 | if (totalTokens === 1) { 82 | title = 'You have 1 guy!'; 83 | } else if (totalTokens < 5) { 84 | title = 'You have ' + totalTokens + ' guys!'; 85 | } else { 86 | title = 'You have a full crew!'; 87 | } 88 | 89 | this.title.text = title; 90 | 91 | if (totalTokens < 5) { 92 | this.moreButton.setInteractive({ useHandCursor: true }); 93 | this.moreButton.alpha = 1; 94 | } else { 95 | this.moreButton.alpha = 0.3; 96 | this.moreButton.removeInteractive(); 97 | } 98 | } 99 | 100 | handleTransfer(event) { 101 | const { from, to, tokenId } = event.returnValues; 102 | const address = this.owner.tokenService.defaultAccount; 103 | if (to == address) { 104 | const existingToken = this.tokens.find(token => { return token.id == tokenId }); 105 | if (!existingToken) { 106 | this.owner.tokenService.getImageId(tokenId).then(imageId => { 107 | this.tokens.push({ id: tokenId, imageId: imageId }); 108 | this.configureTokens(); 109 | }); 110 | } 111 | } else if (from == address) { 112 | this.tokens = this.tokens.filter(token => { return token.id != tokenId }); 113 | this.configureTokens(); 114 | } else { 115 | console.error('Received unexpected event', event); 116 | } 117 | } 118 | 119 | create(config) { 120 | this.owner = config.owner; 121 | this.tokenService = config.tokenService; 122 | this.tokens = config.tokens; 123 | this.characters = []; 124 | 125 | this.make.text({ 126 | x: 0, 127 | y: 1200, 128 | origin: { x: 0, y: 1 }, 129 | padding: 20, 130 | text: "These are the crypto characters you own from our smart contract. Each character is represented by a unique token which determines its appearance.\n\nYou can buy up to five characters using a credit card. To purchase, use this test number: 4242 4242 4242 4242. Use any expiration date and cvv.", 131 | style: styles.explanation 132 | }); 133 | 134 | this.physics.world.setBounds(0, 168, 1100, 500); 135 | 136 | this.title = this.sys.make.text({ 137 | x: 600, 138 | y: 0, 139 | origin: { x: 0.5, y: 0 }, 140 | padding: 20, 141 | style: styles.title 142 | }); 143 | 144 | this.moreButton = this.sys.make.text({ 145 | x: 600, 146 | y: 950, 147 | padding: 20, 148 | origin: { x: 0.5, y: 1 }, 149 | style: styles.primaryButton, 150 | alpha: 0, 151 | text: 'Buy Character: $1.00' 152 | }); 153 | 154 | this.moreButton.on('pointerup', () => { 155 | this.buyToken(); 156 | }); 157 | 158 | this.logOutButton = this.sys.make.text({ 159 | x: 1180, 160 | y: 20, 161 | padding: 20, 162 | origin: { x: 1, y: 0 }, 163 | style: styles.secondaryButton, 164 | text: 'Sign Out' 165 | }); 166 | this.logOutButton.setInteractive({ useHandCursor: true }); 167 | this.logOutButton.on('pointerup', () => { 168 | this.owner.logOut(); 169 | }); 170 | 171 | this.configureTokens(); 172 | 173 | this.owner.tokenService.subscribe((event) => { 174 | this.handleTransfer(event); 175 | }); 176 | } 177 | }; 178 | -------------------------------------------------------------------------------- /client/app/scenes/ReceiptScene.js: -------------------------------------------------------------------------------- 1 | import { Scene } from 'phaser'; 2 | import styles from '../utils/styles'; 3 | 4 | export default class ReceiptScene extends Scene { 5 | constructor() { 6 | super({ key: 'receipt', active: false }); 7 | } 8 | 9 | preload() { 10 | this.load.image('character-1', 'assets/character-1.png'); 11 | this.load.image('character-2', 'assets/character-2.png'); 12 | this.load.image('character-3', 'assets/character-3.png'); 13 | this.load.image('character-4', 'assets/character-4.png'); 14 | this.load.image('character-5', 'assets/character-5.png'); 15 | } 16 | 17 | create(config) { 18 | 19 | const token = config.token; 20 | 21 | this.make.text({ 22 | x: 0, 23 | y: 1200, 24 | origin: { x: 0, y: 1 }, 25 | padding: 20, 26 | text: `Token #${token.id}`, 27 | style: styles.monospaceLabel 28 | }); 29 | 30 | this.make.text({ 31 | x: 0, 32 | y: 1100, 33 | origin: { x: 0, y: 1 }, 34 | padding: 20, 35 | text: `Transaction ${config.transactionHash}`, 36 | style: styles.explanation 37 | }); 38 | 39 | this.owner = config.owner; 40 | 41 | const character = token.imageId; 42 | const characterImageString = `character-${character}`; 43 | const characterImage = this.sys.add.image(600, 600, characterImageString); 44 | characterImage.setScale(1.5); 45 | 46 | characterImage.setInteractive({ useHandCursor: true }); 47 | 48 | characterImage.on('pointerup', () => { 49 | this.owner.showTokenInfo(token.id); 50 | }); 51 | 52 | let backButtonConfig = { 53 | x: 0, 54 | y: 0, 55 | origin: { x: 0, y: 0 }, 56 | padding: 20, 57 | text: 'Back', 58 | style: styles.primaryButton 59 | }; 60 | 61 | let backButton = this.sys.make.text(backButtonConfig); 62 | 63 | backButton.setInteractive({ useHandCursor: true }); 64 | backButton.on('pointerup', this.back, this); 65 | 66 | const viewTransactionButton = this.sys.make.text({ 67 | x: 1200, 68 | y: 0, 69 | origin: { x: 1, y: 0 }, 70 | padding: 20, 71 | text: 'View Txn', 72 | style: styles.primaryButton 73 | }); 74 | 75 | viewTransactionButton.setInteractive({ useHandCursor: true }); 76 | viewTransactionButton.on('pointerup', () => { 77 | this.owner.showTransactionInfo(config.transactionHash); 78 | }); 79 | 80 | this.statusLabel = this.make.text({ 81 | x: 0, 82 | y: 1000, 83 | origin: { x: 0, y: 1 }, 84 | padding: 20, 85 | text: 'Status: Unknown', 86 | style: styles.label 87 | }); 88 | 89 | this.owner.tokenService.watchTransaction(config.transactionHash, (error, receipt) => { 90 | this.transactionUpdated(error, receipt); 91 | }); 92 | } 93 | 94 | transactionUpdated(error, receipt) { 95 | if (error) { 96 | this.statusLabel.text = 'Status: Error!'; 97 | console.error(error); 98 | } else { 99 | if (!receipt) { 100 | this.statusLabel.text = 'Status: Submitted'; 101 | } else if (receipt.status === true) { 102 | this.statusLabel.text = `Status: Confirmed (${receipt.confirmations + 1})` 103 | } else { 104 | this.statusLabel.text = 'Status: Failed'; 105 | } 106 | } 107 | } 108 | 109 | back() { 110 | this.owner.tokenService.stopWatchingTransaction(); 111 | this.scene.stop('receipt'); 112 | this.scene.start('boot'); 113 | } 114 | 115 | deleteToken(token) { 116 | this.scene.start('transaction', { 117 | owner: this.owner, 118 | method: this.owner.tokenService.delete(token.id), 119 | completion: () => { 120 | this.owner.tokenService.list().then((tokens) => { 121 | this.scene.stop('transaction'); 122 | this.scene.start('crew', { tokens: tokens, owner: this.owner }); 123 | }); 124 | } 125 | }); 126 | } 127 | }; 128 | -------------------------------------------------------------------------------- /client/app/scenes/TransactionScene.js: -------------------------------------------------------------------------------- 1 | import { Scene } from 'phaser'; 2 | import styles from '../utils/styles'; 3 | 4 | export default class TransactionScene extends Scene { 5 | constructor() { 6 | super({ key: 'transaction', active: false }); 7 | this.callback = null; 8 | } 9 | 10 | back() { 11 | this.scene.stop('unit'); 12 | this.scene.start('boot'); 13 | } 14 | 15 | create(config) { 16 | 17 | this.owner = config.owner; 18 | 19 | this.make.text({ 20 | x: 0, 21 | y: 0, 22 | origin: { x: 0, y: 1 }, 23 | padding: 20, 24 | text: "Whats Happening?\n\nYou've requested a transation on the ethereum network. That transaction needs to be signed by your wallet. Once it is signed it is submitted to the ethereum network where it will either be accepted or rejected.", 25 | style: styles.explanation 26 | }); 27 | 28 | let message = this.make.text({ 29 | x: 600, 30 | y: 600, 31 | padding: 20, 32 | origin: {x: 0.5, y: 0.5 }, 33 | text: 'Waiting for approval...', 34 | style: styles.label 35 | }); 36 | 37 | if (config.method) { 38 | this.send(config.method, message, config.completion); 39 | } 40 | } 41 | 42 | send(method, message, completion) { 43 | var completionCalled = false; 44 | method.send({ gas: 700000 }) 45 | .on('transactionHash', function (hash) { 46 | if (completionCalled) { 47 | return; 48 | } 49 | message.setText('Waiting for confirmation...'); 50 | }) 51 | .on('confirmation', function (confirmationNumber, receipt) { 52 | if (completionCalled) { 53 | return; 54 | } 55 | if (confirmationNumber >= 1) { 56 | if (completion) { 57 | completion(receipt); 58 | completionCalled = true; 59 | } 60 | } else { 61 | message.setText('Waiting for confirmation...'); 62 | } 63 | }) 64 | .on('error', (error) => { 65 | if (!completionCalled) { 66 | message.setText('Error: ' + error.message); 67 | const back = this.make.text({ 68 | x: 0, 69 | y: 0, 70 | origin: { x: 0, y: 0 }, 71 | padding: 20, 72 | text: 'Back', 73 | style: styles.primaryButton 74 | }); 75 | back.setInteractive({ useHandCursor: true }); 76 | back.on('pointerup', this.back, this); 77 | } 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/app/scenes/UnitScene.js: -------------------------------------------------------------------------------- 1 | import { Scene } from 'phaser'; 2 | import styles from '../utils/styles'; 3 | 4 | export default class UnitScene extends Scene { 5 | constructor() { 6 | super({ key: 'unit', active: false }); 7 | } 8 | 9 | preload() { 10 | this.load.image('character-1', 'assets/character-1.png'); 11 | this.load.image('character-2', 'assets/character-2.png'); 12 | this.load.image('character-3', 'assets/character-3.png'); 13 | this.load.image('character-4', 'assets/character-4.png'); 14 | this.load.image('character-5', 'assets/character-5.png'); 15 | } 16 | 17 | create(config) { 18 | const token = config.token; 19 | 20 | this.make.text({ 21 | x: 0, 22 | y: 1200, 23 | origin: { x: 0, y: 1 }, 24 | padding: 20, 25 | text: `Token #${config.token.id}`, 26 | style: styles.monospaceLabel 27 | }); 28 | 29 | this.owner = config.owner; 30 | 31 | const character = token.imageId; 32 | const characterImageString = `character-${character}`; 33 | const characterImage = this.sys.add.image(600, 600, characterImageString); 34 | characterImage.setScale(1.5); 35 | characterImage.setInteractive({ useHandCursor: true }); 36 | characterImage.on('pointerup', () => { 37 | this.owner.showTokenInfo(token.id); 38 | }); 39 | 40 | let backButtonConfig = { 41 | x: 0, 42 | y: 0, 43 | origin: { x: 0, y: 0 }, 44 | padding: 20, 45 | text: 'Back', 46 | style: styles.primaryButton 47 | }; 48 | 49 | let backButton = this.sys.make.text(backButtonConfig); 50 | 51 | backButton.setInteractive({ useHandCursor: true }); 52 | backButton.on('pointerup', this.back, this); 53 | 54 | let transferButton = this.sys.make.text({ 55 | x: 1200, 56 | y: 0, 57 | padding: 20, 58 | origin: { x: 1, y: 0 }, 59 | text: 'Transfer', 60 | style: styles.secondaryButton 61 | }); 62 | 63 | transferButton.setInteractive({ useHandCursor: true }); 64 | transferButton.on('pointerup', (event) => { 65 | this.transferToken(token); 66 | }); 67 | } 68 | 69 | back() { 70 | this.scene.stop('unit'); 71 | this.scene.start('boot'); 72 | } 73 | 74 | transferToken(token) { 75 | this.owner.showTransferUI(token); 76 | } 77 | 78 | deleteToken(token) { 79 | this.scene.start('transaction', { 80 | owner: this.owner, 81 | method: this.owner.tokenService.delete(token.id), 82 | completion: () => { 83 | this.owner.tokenService.list().then((tokens) => { 84 | this.scene.stop('transaction'); 85 | this.scene.start('crew', { tokens: tokens, owner: this.owner }); 86 | }); 87 | } 88 | }); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /client/app/services/AppWalletService.js: -------------------------------------------------------------------------------- 1 | export default class AppWalletService { 2 | 3 | constructor(baseUrl) { 4 | this.baseUrl = baseUrl; 5 | } 6 | 7 | processPurchase(token, recipient) { 8 | return fetch(`${this.baseUrl}/process-transaction`, { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json' 12 | }, 13 | body: JSON.stringify({ token, recipient }) 14 | }).then(response => { 15 | return response.json().then(json => { 16 | if (response.status > 199 && response.status < 300) { 17 | return json; 18 | } 19 | return this.parseError(json); 20 | }); 21 | }); 22 | } 23 | 24 | parseError(response) { 25 | if (response.error) { 26 | const error = response.error; 27 | if (error.message) { 28 | throw new Error(error.message); 29 | } else { 30 | throw new Error(error); 31 | } 32 | } else { 33 | throw new Error('Something went wrong. Unknown error.'); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /client/app/services/StripeService.js: -------------------------------------------------------------------------------- 1 | export default class StripeService { 2 | 3 | constructor(apiKey) { 4 | this.apiKey = apiKey; 5 | this.storeName = 'Bitski App Wallet Demo'; 6 | 7 | this.handler = StripeCheckout.configure({ 8 | key: apiKey, 9 | image: 'https://stripe.com/img/documentation/checkout/marketplace.png', 10 | locale: 'auto', 11 | token: (token) => { 12 | if (this.tokenHandler) { 13 | if (token) { 14 | this.tokenHandler.fulfill(token); 15 | } else { 16 | this.tokenHandler.reject(new Error('Transaction was cancelled')); 17 | } 18 | this.tokenHandler = undefined; 19 | } 20 | } 21 | }); 22 | } 23 | 24 | showCheckoutForm(itemName, amount) { 25 | return new Promise((fulfill, reject) => { 26 | this.tokenHandler = { fulfill, reject }; 27 | this.handler.open({ 28 | name: this.storeName, 29 | description: itemName, 30 | amount: amount 31 | }); 32 | }); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /client/app/services/TokenService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This JSON file was created by Truffle and contains the ABI of our contract 3 | * as well as the address for any networks we have deployed it to. 4 | */ 5 | const Token = require('../../../build/contracts/LimitedMintableNonFungibleToken.json'); 6 | 7 | export default class TokenService { 8 | /** 9 | * Since our contract will have different addresses depending on which network 10 | * it is deployed on we need to load the network ID before we can initialize the 11 | * contract. This will happen async. 12 | */ 13 | static currentNetwork(web3) { 14 | return web3.eth.net.getId().then(function(networkID){ 15 | return web3.eth.getAccounts().then(function(accounts){ 16 | const contractAddress = TokenService.loadDeployedAddress(networkID); 17 | return new TokenService(web3, contractAddress, accounts[0]); 18 | }); 19 | }); 20 | } 21 | 22 | static loadDeployedAddress(networkID) { 23 | if (Token.networks && Token.networks[networkID] && Token.networks[networkID].address) { 24 | return Token.networks[networkID].address; 25 | } else { 26 | throw Error(`Contract not deployed on current network (${networkID}). Run truffle migrate first and try again.`); 27 | } 28 | } 29 | 30 | constructor(web3, address, defaultAccount) { 31 | if (!address) { 32 | throw new Error('Contract address not provided'); 33 | } 34 | 35 | if (!Token || !Token.abi) { 36 | throw new Error('Contract not compiled or not found'); 37 | } 38 | 39 | this.web3 = web3; 40 | this.defaultAccount = defaultAccount; 41 | 42 | const abi = Token.abi; 43 | this.address = address; 44 | this.contract = new web3.eth.Contract(abi, address); 45 | this.contract.setProvider(this.web3.currentProvider); 46 | 47 | if (this.defaultAccount) { 48 | this.contract.defaultAccount = this.defaultAccount; 49 | this.contract.options.from = this.defaultAccount; 50 | } 51 | } 52 | 53 | /** 54 | * Deletes a token by transfering it to the contract address. 55 | * 56 | * @param {string} token the ID of the token we want to delete. 57 | */ 58 | delete(token) { 59 | return this.contract.methods.burn(token); 60 | } 61 | 62 | transfer(token, recipient) { 63 | return this.contract.methods.transferFrom(this.defaultAccount, recipient, token); 64 | } 65 | 66 | /** 67 | * Gets a list of all tokens owned by us. 68 | */ 69 | list() { 70 | return this.contract.methods.getOwnerTokens(this.defaultAccount).call().then(tokenIds => { 71 | const promises = tokenIds.map(tokenId => { 72 | return this.getImageId(tokenId).then(imageId => { 73 | return { id: tokenId, imageId: imageId }; 74 | }); 75 | }); 76 | return Promise.all(promises); 77 | }); 78 | } 79 | 80 | getImageId(tokenId) { 81 | return this.contract.methods.imageId(tokenId).call(); 82 | } 83 | 84 | watchTransaction(hash, callback) { 85 | this.transactionSubscription = this.web3.eth.subscribe('newBlockHeaders').on('data', (block) => { 86 | const blockNumber = block.number; 87 | this.web3.eth.getTransactionReceipt(hash).then(receipt => { 88 | if (receipt) { 89 | receipt.confirmations = blockNumber - receipt.blockNumber; 90 | } 91 | callback(undefined, receipt); 92 | }, error => callback(error)); 93 | }); 94 | } 95 | 96 | stopWatchingTransaction() { 97 | this.transactionSubscription.unsubscribe(); 98 | } 99 | 100 | /** 101 | * Gets a count of our tokens. 102 | */ 103 | balance() { 104 | return this.contract.methods.balanceOf(this.contract.defaultAccount).call(); 105 | } 106 | 107 | subscribe(callback) { 108 | this.subscription = this.contract.events.Transfer().on('data', (event) => { 109 | if (event.returnValues.to == this.defaultAccount || event.returnValues.from == this.defaultAccount) { 110 | callback(event); 111 | } 112 | }); 113 | } 114 | 115 | unsubscribe() { 116 | if (this.subscription) { 117 | this.subscription.unsubscribe(); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /client/app/utils/styles.js: -------------------------------------------------------------------------------- 1 | 2 | const text = { 3 | fontFamily: 'Acme' 4 | }; 5 | 6 | const label = Object.assign({}, text, { 7 | color: '#fff', 8 | fontSize: '32px', 9 | }); 10 | 11 | const title = Object.assign({}, label, { 12 | align: 'center', 13 | backgroundColor: '#2DAA58', 14 | fontSize: '64px' 15 | }); 16 | 17 | const button = Object.assign({}, label, { 18 | fontSize: '64px', 19 | align: 'center', 20 | }); 21 | 22 | const primaryButton = Object.assign({}, button, { 23 | backgroundColor: '#2B67AB' 24 | }); 25 | 26 | const secondaryButton = Object.assign({}, button, { 27 | backgroundColor: '#444' 28 | }); 29 | 30 | const negativeButton = Object.assign({}, button, { 31 | backgroundColor: '#E95C3B' 32 | }); 33 | 34 | const explanation = Object.assign({}, label, { 35 | backgroundColor: '#333333', 36 | wordWrap: { width: 1160 } 37 | }); 38 | 39 | const monospaceLabel = Object.assign({}, label, { 40 | fontFamily: 'Courier', 41 | fontSize: '22px', 42 | backgroundColor: '#333333', 43 | wordWrap: { width: 1200 } 44 | }); 45 | 46 | const styles = { 47 | text, 48 | label, 49 | title, 50 | button, 51 | primaryButton, 52 | secondaryButton, 53 | negativeButton, 54 | explanation, 55 | monospaceLabel 56 | }; 57 | 58 | export default styles; 59 | -------------------------------------------------------------------------------- /client/app/views/LoggedIn.js: -------------------------------------------------------------------------------- 1 | export class LoggedInView { 2 | 3 | constructor(selector) { 4 | this.selector = selector; 5 | if (document.readyState === 'complete') { 6 | this._assignElement(); 7 | } else { 8 | window.addEventListener('load', this._assignElement.bind(this)); 9 | } 10 | } 11 | 12 | _assignElement() { 13 | this.element = document.querySelector(this.selector); 14 | this.gameContainer = this.element; 15 | } 16 | 17 | show() { 18 | this.element.style.display = 'block'; 19 | } 20 | 21 | hide() { 22 | this.element.style.display = 'none'; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /client/app/views/LoggedOut.js: -------------------------------------------------------------------------------- 1 | export class LoggedOutView { 2 | 3 | constructor(selector, bitski, callback) { 4 | this.bitski = bitski; 5 | this.selector = selector; 6 | this.loginCallback = callback; 7 | if (document.readyState === 'complete') { 8 | this._assignElement(); 9 | } else { 10 | window.addEventListener('load', this._assignElement.bind(this)); 11 | } 12 | } 13 | 14 | _assignElement() { 15 | this.element = document.querySelector(this.selector); 16 | this.errorMessage = this.element.querySelector('#error'); 17 | this.connectButtonContainer = this.element.querySelector('#connect-button'); 18 | this.metamaskButton = this.element.querySelector('#metamask-button'); 19 | this.configureView(); 20 | } 21 | 22 | configureView() { 23 | this.createLoginButton(); 24 | this.configureMetamaskButton(); 25 | } 26 | 27 | show() { 28 | this.element.style.display = 'flex'; 29 | } 30 | 31 | hide() { 32 | this.element.style.display = 'none'; 33 | } 34 | 35 | setError(error) { 36 | if (error) { 37 | this.errorMessage.innerText = (error && error.message) || error 38 | console.error(error); 39 | } else { 40 | this.errorMessage.innerText = ''; 41 | } 42 | } 43 | 44 | createLoginButton() { 45 | this.bitski.getConnectButton({ container: this.connectButtonContainer, size: 'LARGE' }, (error, user) => { 46 | if (error) { 47 | this.setError(error); 48 | return; 49 | } 50 | 51 | if (user && this.loginCallback) { 52 | this.loginCallback(this.bitski.getProvider(BITSKI_PROVIDER_ID)); 53 | } 54 | }); 55 | } 56 | 57 | logInMetaMask() { 58 | window.ethereum.enable().then(() => { 59 | if (this.loginCallback) { 60 | this.loginCallback(window.ethereum); 61 | } 62 | }).catch(error => { 63 | this.setError(error); 64 | }); 65 | } 66 | 67 | configureMetamaskButton() { 68 | this.metamaskButton.addEventListener('click', () => { 69 | this.logInMetaMask(); 70 | }); 71 | 72 | if (window.ethereum) { 73 | this.metamaskButton.style.display = 'block'; 74 | } else { 75 | this.metamaskButton.style.display = 'none'; 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /client/app/views/Transfer.js: -------------------------------------------------------------------------------- 1 | const TEMPLATE = ` 2 |
3 |
4 |

Transfer Token

5 |

Enter the ethereum address to send this token to:

6 |
7 |
8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | `; 16 | 17 | export class TransferModal { 18 | 19 | constructor(token, callback) { 20 | this.token = token; 21 | this.callback = callback; 22 | this.container = document.getElementById('modal-container'); 23 | } 24 | 25 | show() { 26 | this.container.innerHTML = TEMPLATE; 27 | this.container.classList.add('visible'); 28 | const recipientField = this.container.querySelector('#transfer-modal input[name=recipient]'); 29 | const submitButton = this.container.querySelector('#transfer-modal button.submit'); 30 | const cancelButton = this.container.querySelector("#transfer-modal button.cancel"); 31 | 32 | this.container.addEventListener('mouseup', (e) => { 33 | e.stopPropagation(); 34 | }); 35 | 36 | submitButton.addEventListener('click', (e) => { 37 | const recipient = recipientField.value; 38 | this.callback(recipient); 39 | this.hide(); 40 | }); 41 | 42 | cancelButton.addEventListener('click', (e) => { 43 | this.hide(); 44 | }); 45 | } 46 | 47 | hide() { 48 | this.container.classList.remove('visible'); 49 | this.container.innerHTML = ''; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /client/assets/character-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitskiCo/example-stripe-nft/1f7e375d2a52686003b943a75a8b8654e7088279/client/assets/character-1.png -------------------------------------------------------------------------------- /client/assets/character-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitskiCo/example-stripe-nft/1f7e375d2a52686003b943a75a8b8654e7088279/client/assets/character-2.png -------------------------------------------------------------------------------- /client/assets/character-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitskiCo/example-stripe-nft/1f7e375d2a52686003b943a75a8b8654e7088279/client/assets/character-3.png -------------------------------------------------------------------------------- /client/assets/character-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitskiCo/example-stripe-nft/1f7e375d2a52686003b943a75a8b8654e7088279/client/assets/character-4.png -------------------------------------------------------------------------------- /client/assets/character-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitskiCo/example-stripe-nft/1f7e375d2a52686003b943a75a8b8654e7088279/client/assets/character-5.png -------------------------------------------------------------------------------- /client/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitskiCo/example-stripe-nft/1f7e375d2a52686003b943a75a8b8654e7088279/client/assets/screenshot.png -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-demo-client", 3 | "description": "The client-side application, responsible for showing your tokens and collecting credit card info", 4 | "scripts": { 5 | "start": "node server.js", 6 | "build": "NODE_ENV=production webpack", 7 | "dev": "webpack-dev-server --mode development --port 3000" 8 | }, 9 | "dependencies": { 10 | "dotenv": "^6.2.0", 11 | "express": "^4.16.4" 12 | }, 13 | "devDependencies": { 14 | "babel-core": "^6.26.0", 15 | "babel-loader": "^7.1.4", 16 | "babel-preset-env": "^1.6.1", 17 | "bitski": "^0.10.1", 18 | "copy-webpack-plugin": "^4.5.1", 19 | "html-webpack-plugin": "^3.2.0", 20 | "phaser": "^3.16.2", 21 | "raven-js": "^3.26.2", 22 | "raw-loader": "^0.5.1", 23 | "truffle": "^4.1.14", 24 | "web3": "1.0.0-beta.55", 25 | "webpack": "^4.6.0", 26 | "webpack-cli": "^3.1.1", 27 | "webpack-dev-server": "^3.1.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/public/callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logging in... 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 3 | font-size: 14px; 4 | margin: 0; 5 | padding: 0; 6 | background: #000; 7 | color: #fff; 8 | } 9 | 10 | h1, h2, h3, h4, h5, h6 { 11 | font-family: 'Acme', sans-serif; 12 | } 13 | 14 | .container { 15 | margin: 0 auto; 16 | max-width: 600px; 17 | } 18 | 19 | #error { 20 | color: #b10009; 21 | } 22 | 23 | a { 24 | color: #fff; 25 | font-weight: 600; 26 | text-decoration: none; 27 | } 28 | 29 | a:hover { 30 | color: #fff; 31 | text-decoration: underline; 32 | } 33 | 34 | a:visited { 35 | color: #fff; 36 | } 37 | 38 | #metamask-button { 39 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 'Helvetica Neue', sans-serif; 40 | font-weight: bold; 41 | font-size: 12px; 42 | background-color: #F79220; 43 | background-position-y: 50%; 44 | background-repeat: no-repeat; 45 | color: #fff; 46 | border: none; 47 | margin: 0; 48 | padding: 0 14px; 49 | cursor: pointer; 50 | border-radius: 6px; 51 | height: 28px; 52 | line-height: 28px; 53 | display: block; 54 | width: 100%; 55 | margin-top: 20px; 56 | } 57 | 58 | .bitski-connect-button { 59 | display: block; 60 | width: 100%; 61 | } 62 | 63 | #signed-out { 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | position: fixed; 68 | top: 0; 69 | bottom: 0; 70 | left: 0; 71 | right: 0; 72 | } 73 | 74 | #signed-out > div { 75 | width: 300px; 76 | text-align: center; 77 | } 78 | 79 | #signed-out .box { 80 | background: #111; 81 | padding: 20px; 82 | border-radius: 10px; 83 | margin-bottom: 20px; 84 | } 85 | 86 | #modal-container { 87 | position: fixed; 88 | top: 0; 89 | left: 0; 90 | right: 0; 91 | bottom: 0; 92 | display: none; 93 | opacity: 0; 94 | background: rgba(0,0,0,0.3); 95 | z-index: 1000; 96 | } 97 | 98 | #modal-container.visible { 99 | display: flex; 100 | align-items: center; 101 | justify-content: center; 102 | opacity: 1; 103 | } 104 | 105 | #modal-container #transfer-modal { 106 | width: 375px; 107 | background: #333; 108 | padding: 30px; 109 | border-radius: 20px; 110 | overflow: hidden; 111 | } 112 | 113 | #transfer-modal input { 114 | padding: 0.5rem; 115 | font-size: 0.8rem; 116 | margin-bottom: 1rem; 117 | display: block; 118 | width: 100%; 119 | box-sizing: border-box; 120 | } 121 | 122 | #transfer-modal button { 123 | background: #555; 124 | color: #fff; 125 | border: none; 126 | font-size: 1rem; 127 | font-weight: 500; 128 | outline: none; 129 | border-radius: 2px; 130 | padding: 0.5rem 1.5rem; 131 | cursor: pointer; 132 | } 133 | 134 | #transfer-modal button.submit { 135 | background-color: #2B67AB; 136 | } 137 | 138 | #transfer-modal h3 { 139 | margin-top: 0; 140 | } 141 | -------------------------------------------------------------------------------- /client/server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | var path = require('path'); 3 | var express = require('express'); 4 | 5 | var app = express(); 6 | 7 | app.use(express.static(path.join(__dirname, 'dist'))); 8 | app.set('port', process.env.PORT || 8080); 9 | 10 | var server = app.listen(app.get('port'), function() { 11 | console.log('listening on port ', server.address().port); 12 | }); 13 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | 7 | const networkIds = { 8 | mainnet: 1, 9 | rinkeby: 4, 10 | kovan: 42, 11 | }; 12 | 13 | module.exports = env => { 14 | // Configuration options 15 | const environment = process.env.NODE_ENV || 'development'; 16 | const networkName = process.env.NETWORK_NAME || 'rinkeby'; 17 | const networkId = networkIds[networkName]; 18 | const bitskiClientId = process.env.BITSKI_CLIENT_ID || false; 19 | const bitskiRedirectURL = process.env.BITSKI_REDIRECT_URL || 'http://localhost:3000/public/callback.html'; 20 | const contractAddress = process.env.CONTRACT_ADDRESS || false; 21 | const sentryDSN = environment == 'production' && process.env.SENTRY_DSN || false; 22 | const devtool = environment == 'development' ? 'source-map' : false; 23 | 24 | return { 25 | devtool: devtool, 26 | entry: './app/index.js', 27 | 28 | output: { 29 | filename: 'index.js', 30 | path: path.resolve(__dirname, 'dist') 31 | }, 32 | module: { 33 | rules: [{ 34 | test: [/\.vert$/, /\.frag$/], 35 | use: 'raw-loader' 36 | }, { 37 | test: /\.js$/, 38 | exclude: /node_modules/, 39 | loader: 'babel-loader', 40 | 41 | options: { 42 | presets: ['env'] 43 | } 44 | } 45 | ] 46 | }, 47 | 48 | plugins: [ 49 | new HTMLWebpackPlugin({ 50 | title: 'Example Dapp', 51 | template: './app/index.html', 52 | hash: true 53 | }), 54 | new CopyWebpackPlugin([ 55 | { 56 | from: 'assets', 57 | to: 'assets' 58 | }, { 59 | from: 'public', 60 | to: 'public' 61 | } 62 | ]), 63 | new webpack.DefinePlugin({ 64 | 'CANVAS_RENDERER': JSON.stringify(true), 65 | 'WEBGL_RENDERER': JSON.stringify(true), 66 | 'BITSKI_PROVIDER_ID': JSON.stringify(networkName), 67 | 'EXPECTED_NETWORK_NAME': JSON.stringify(networkName), 68 | 'EXPECTED_NETWORK_ID': JSON.stringify(networkId), 69 | 'BITSKI_CLIENT_ID': JSON.stringify(bitskiClientId), 70 | 'BITSKI_REDIRECT_URL': JSON.stringify(bitskiRedirectURL), 71 | 'CONTRACT_ADDRESS': JSON.stringify(contractAddress), 72 | 'SENTRY_DSN': JSON.stringify(sentryDSN), 73 | 'STRIPE_API_KEY': JSON.stringify(process.env.STRIPE_API_KEY || false), 74 | 'APP_WALLET_URL': JSON.stringify(process.env.APP_WALLET_URL || false), 75 | }) 76 | ] 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /contracts/ExampleApp.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./LimitedMintableNonFungibleToken.sol"; 4 | 5 | /** 6 | This contract acts as a gateway between App Wallet and the token contract. 7 | We register this contract with the token as a minter, and this contract ensures that 8 | only the App Wallet can mint from here. By separating out the App Wallet interface from the 9 | basic token interface, we can perform upgrades to application functionality without touching the token. 10 | */ 11 | contract ExampleApp { 12 | 13 | LimitedMintableNonFungibleToken token; 14 | address appWallet; 15 | 16 | modifier onlyAppWallet() { 17 | require (msg.sender == appWallet); 18 | _; 19 | } 20 | 21 | constructor(address tokenAddress, address appWalletAddress) public { 22 | appWallet = appWalletAddress; 23 | token = LimitedMintableNonFungibleToken(tokenAddress); 24 | } 25 | 26 | function setAppWalletAddress(address _newAddress) public onlyAppWallet { 27 | appWallet = _newAddress; 28 | } 29 | 30 | function mint(address _to, uint256 _tokenId, string memory _tokenURI) public onlyAppWallet returns (bool) { 31 | return token.mintWithTokenURI(_to, _tokenId, _tokenURI); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /contracts/LimitedMintableNonFungibleToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC721/ERC721Full.sol"; 4 | import "openzeppelin-solidity/contracts/access/roles/MinterRole.sol"; 5 | import "openzeppelin-solidity/contracts/token/ERC721/ERC721Burnable.sol"; 6 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 7 | 8 | /** 9 | * @title LimitedMintableNonFungibleToken 10 | * 11 | * Superset of the ERC721 standard that allows for the minting 12 | * of non-fungible tokens, but limited to n tokens. 13 | */ 14 | contract LimitedMintableNonFungibleToken is ERC721Full, MinterRole, ERC721Burnable { 15 | 16 | using SafeMath for uint256; 17 | 18 | // The maximum amount of tokens that can be owned by an address 19 | uint public mintLimit; 20 | 21 | // The number of character designs 22 | uint public characterCount; 23 | 24 | /** 25 | * @dev Initializes the contract with a mint limit 26 | * @param _mintLimit the maximum tokens a given address may own at a given time 27 | * @param _characterCount the number of unique character designs 28 | */ 29 | constructor(uint _mintLimit, uint _characterCount) ERC721Full("Bitski Example Dude", "BED") public { 30 | mintLimit = _mintLimit; 31 | characterCount = _characterCount; 32 | } 33 | 34 | /** 35 | * @dev Mints a new token with the given id to the given address 36 | * @param _to the owner of the token 37 | * @param _tokenId the id of the token to mint 38 | */ 39 | function mint(address _to, uint256 _tokenId) public onlyMinter returns (bool) { 40 | // Enforce the mint limit 41 | require(balanceOf(_to) < mintLimit, "You have reached the token limit"); 42 | _mint(_to, _tokenId); 43 | return true; 44 | } 45 | 46 | /** 47 | * @dev Mints a new token with the given id to the given address, and associates a token uri with it 48 | * @param _to the owner of the token 49 | * @param _tokenId the id of the token to mint 50 | * @param _tokenURI the URI containing the metadata about this token 51 | */ 52 | function mintWithTokenURI(address _to, uint256 _tokenId, string memory _tokenURI) public onlyMinter returns (bool) { 53 | require(balanceOf(_to) < mintLimit, "You have reached the token limit"); 54 | _mint(_to, _tokenId); 55 | _setTokenURI(_tokenId, _tokenURI); 56 | return true; 57 | } 58 | 59 | /** 60 | * @dev Returns the token ids owned by the given address 61 | * @param _owner the owner to query 62 | */ 63 | function getOwnerTokens(address _owner) external view returns (uint256[] memory) { 64 | uint256 balance = balanceOf(_owner); 65 | uint256[] memory tokenIds = new uint256[](balance); 66 | for (uint i = 0; i < balance; i++) { 67 | tokenIds[i] = tokenOfOwnerByIndex(_owner, i); 68 | } 69 | return tokenIds; 70 | } 71 | 72 | /** 73 | * @dev Returns the character image id for a given token 74 | * @param _tokenId the token id 75 | */ 76 | function imageId(uint256 _tokenId) external view returns(uint256) { 77 | require(_exists(_tokenId), "Token ID must be valid"); 78 | uint256 index = _tokenId.mod(characterCount); 79 | uint256 result = index.add(1); 80 | return result; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.6.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_initialize_nft.js: -------------------------------------------------------------------------------- 1 | var LimitedMintableNonFungibleToken = artifacts.require("LimitedMintableNonFungibleToken"); 2 | var ExampleApp = artifacts.require("ExampleApp"); 3 | 4 | module.exports = function(deployer, network, accounts) { 5 | deployer.deploy(LimitedMintableNonFungibleToken, 5, 5).then(() => { 6 | return deployer.deploy(ExampleApp, LimitedMintableNonFungibleToken.address, accounts[0]); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /migrations/3_add_minter.js: -------------------------------------------------------------------------------- 1 | var LimitedMintableNonFungibleToken = artifacts.require("LimitedMintableNonFungibleToken"); 2 | var ExampleApp = artifacts.require("ExampleApp"); 3 | 4 | module.exports = function(deployer, network, accounts) { 5 | return LimitedMintableNonFungibleToken.deployed().then(instance => { 6 | return instance.isMinter(ExampleApp.address).then(result => { 7 | if (result.valueOf() === false) { 8 | return instance.addMinter(ExampleApp.address, { from: accounts[0] }); 9 | } 10 | }); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "truffle compile", 4 | "test": "truffle test" 5 | }, 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "bitski-node": "^0.7.1", 9 | "dotenv": "^6.2.0", 10 | "openzeppelin-solidity": "^2.1.1", 11 | "truffle": "^5.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Stripe Demo - Backend App 2 | 3 | The server component of this demo is responsible for processing payments, creating tokens on behalf of users, and serving metadata for the token contract. We've written this demo in Node.js, and it uses a lightweight Express server to serve API requests. It uses Web3.js with Bitski's Node SDK to connect to the Ethereum network, and a Bitski App Wallet to submit transactions. 4 | 5 | A live version of this code is running at [https://stripe-demo-api.bitski.com](https://stripe-demo-api.bitski.com). 6 | 7 | ## Requirements 8 | 9 | - NPM 10 | - Node 11 | - A Bitski app ([https://developer.bitski.com](https://developer.bitski.com)) 12 | - A Stripe account ([https://dashboard.stripe.com]) 13 | 14 | ## Running the demo 15 | 16 | First, make sure you've followed the steps to compile and migrate the contracts at the root of this repo. 17 | 18 | ### Install Dependencies 19 | 20 | ``` 21 | npm install 22 | ``` 23 | 24 | ### Runtime Environment Variables 25 | 26 | To properly configure the server, create a .env file in the server directory, and define the following environment variables (see [dotenv](https://github.com/motdotla/dotenv) for more info): 27 | 28 | - STRIPE_SECRET: Your Stripe secret key (Available under Developers > API Keys in the Stripe Dashboard) 29 | - BITSKI_APP_WALLET_ID: Your Bitski App Credential ID (Available under Backend Credentials on developer.bitski.com) 30 | - BITSKI_APP_WALLET_SECRET: Your Bitski App Credential ID (Available under Backend Credentials on developer.bitski.com) 31 | - WEB_URL: The URL for the client application - used for token metadata and image assets 32 | - API_URL: The URL for the server application - used for the token URI 33 | - PORT: Optionally specify what port to deploy the server on 34 | 35 | Alternatively, you can export these environment variables in the command line. 36 | 37 | ### Start the server 38 | 39 | ``` 40 | npm start 41 | ``` 42 | -------------------------------------------------------------------------------- /server/app/app.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3'); 2 | const Bitski = require('bitski-node'); 3 | const Contract = require('./contract'); 4 | const ExampleAppABI = require('../../build/contracts/ExampleApp'); 5 | const TokenABI = require('../../build/contracts/LimitedMintableNonFungibleToken'); 6 | const BN = require('bn.js'); 7 | const stripe = require('stripe')(process.env.STRIPE_SECRET); 8 | const Transaction = require('./transaction'); 9 | const Server = require('./server'); 10 | const fetch = require('node-fetch'); 11 | 12 | const GAS_PRICE = '1100000000'; // Ideally this would be dynamically updated based on demand. 13 | const MIN_CONFIRMATIONS = 2; // Minimum # of confirmations required before finalizing charges 14 | 15 | class App { 16 | 17 | /** 18 | * Creates a new instance of App 19 | * @param {string} clientId Your Bitski client id 20 | * @param {string} network The network name to use (mainnet | kovan | rinkeby) 21 | * @param {object} credentials Your app wallet credentials 22 | * @param {string} credentials.id Your credential id 23 | * @param {string} credentials.secret Your credential secret 24 | */ 25 | constructor(clientId, network, credentials) { 26 | const options = { 27 | credentials: credentials, 28 | network: network 29 | }; 30 | // Create instance of BitskiProvider 31 | this.provider = Bitski.getProvider(clientId, options); 32 | 33 | // Create instance of web3 34 | this.web3 = new Web3(this.provider); 35 | } 36 | 37 | /** 38 | * Starts the app by connecting to Ethereum network and starting the Express server 39 | * @param {number} port Port to start the server on 40 | */ 41 | async start(port) { 42 | console.log('starting app...'); 43 | try { 44 | // Get accounts 45 | const accounts = await this.web3.eth.getAccounts(); 46 | // Check to make sure we have an account 47 | if (accounts.length == 0) { 48 | throw new Error('No account found'); 49 | } 50 | 51 | // Set current account 52 | this.currentAccount = accounts[0]; 53 | 54 | this.balance = await this.web3.eth.getBalance(this.currentAccount); 55 | 56 | // Set network id 57 | this.networkId = await this.web3.eth.net.getId(); 58 | 59 | // Create instance of contract 60 | this.contract = await new Contract(this.web3, this.networkId, ExampleAppABI).deployed(); 61 | this.token = await new Contract(this.web3, this.networkId, TokenABI).deployed(); 62 | 63 | // Cache token name 64 | this.name = await this.token.methods.name().call(); 65 | 66 | // Create the server 67 | this.server = new Server(port, this); 68 | 69 | // Refresh balance every 60 seconds 70 | this.updateBalance(); 71 | 72 | // Watch for new events 73 | this.watchTransferEvents(); 74 | 75 | 76 | } catch (error) { 77 | console.error(error); 78 | process.exit(1); 79 | } 80 | } 81 | 82 | /** 83 | * Watches for new Transfer events from this contract and logs them to the console 84 | */ 85 | watchTransferEvents() { 86 | this.token.events.Transfer().on('data', (event) => { 87 | const { to, from, tokenId } = event.returnValues; 88 | console.log(`Token ${tokenId} was transferred from ${from} to ${to}`); 89 | }).on('error', (error) => { 90 | console.log('Error subscribing', error); 91 | const now = new Date(); 92 | console.log(now.toISOString()); 93 | }); 94 | } 95 | 96 | /** 97 | * We regularly check the balance of our App Wallet to make sure we're still funded. 98 | */ 99 | updateBalance() { 100 | setTimeout(() => { 101 | this.web3.eth.getBalance(this.currentAccount).then(balance => { 102 | this.balance = balance; 103 | console.log(`Current balance: ${balance}`); 104 | }).catch(error => { 105 | console.log('Error updating balance:'); 106 | console.error(error); 107 | const now = new Date(); 108 | console.log(now.toISOString()); 109 | }); 110 | this.updateBalance(); 111 | }, 60 * 1000); 112 | } 113 | 114 | getTotalSupply() { 115 | return this.token.methods.totalSupply().call(); 116 | } 117 | 118 | getName() { 119 | return this.token.methods.name().call(); 120 | } 121 | 122 | getSymbol() { 123 | return this.token.methods.symbol().call(); 124 | } 125 | 126 | getMintLimit() { 127 | return this.token.methods.mintLimit().call(); 128 | } 129 | 130 | getBalance(owner) { 131 | return this.token.methods.balanceOf(owner).call(); 132 | } 133 | 134 | getTokens(owner) { 135 | return this.token.methods.balanceOf(owner).call().then(balance => { 136 | let promises = []; 137 | for (var i = 0; i < balance; i++) { 138 | const promise = this.token.methods.tokenOfOwnerByIndex(owner, i).call(); 139 | promises.push(promise); 140 | } 141 | return Promise.all(promises); 142 | }); 143 | } 144 | 145 | getTokenURI(tokenId) { 146 | return this.token.methods.tokenURI(tokenId).call(); 147 | } 148 | 149 | getTokenMetadata(tokenId) { 150 | // Load character index from the contract (used to determine which image asset to return) 151 | return this.token.methods.imageId(tokenId).call().then(imageIndex => { 152 | const baseUrl = process.env.WEB_URL || 'https://example-dapp-1.bitski.com'; 153 | const description = 'An example of an ERC-721 token'; 154 | const name = this.name; // this is loaded from the contract when we boot 155 | const imageUrl = `${baseUrl}/assets/character-${imageIndex}.png`; 156 | 157 | //The ERC-721 Metadata standard 158 | const erc721Details = { 159 | name: name, 160 | description: description, 161 | image: imageUrl 162 | }; 163 | 164 | // Additional OpenSea Metadata 165 | const openSeaExtras = { 166 | external_url: baseUrl, 167 | }; 168 | 169 | // Additional RareBits Metadata 170 | const rareBitsExtras = { 171 | image_url: imageUrl, 172 | home_url: baseUrl 173 | }; 174 | 175 | return Object.assign({}, erc721Details, openSeaExtras, rareBitsExtras); 176 | }); 177 | } 178 | 179 | createMintTransaction(recipient) { 180 | // Create the transaction inputs 181 | const tokenId = this.web3.utils.randomHex(32); 182 | const tokenIdString = this.web3.utils.hexToNumberString(tokenId); 183 | 184 | // Generate the token URI (points at this app) 185 | const baseUrl = process.env.API_URL || 'https://example-dapp-1-api.bitski.com'; 186 | const tokenURI = `${baseUrl}/tokens/${tokenIdString}`; 187 | 188 | // Calculate NFT metadata 189 | const BN = this.web3.utils.BN; 190 | const expectedImageId = this.web3.utils.toBN(tokenIdString).mod(new BN(5)).add(new BN(1)).toString(); 191 | 192 | // Create "transaction" object to manage state of the transaction 193 | const transaction = new Transaction(this.web3, this.contract.methods.mint); 194 | transaction.setInputs(recipient, tokenId, tokenURI); 195 | return {transaction, tokenIdString, expectedImageId}; 196 | } 197 | 198 | processStripeTransaction(token, recipient) { 199 | const {transaction, tokenIdString, expectedImageId} = this.createMintTransaction(recipient); 200 | 201 | return transaction.validate(this.currentAccount).then(() => { 202 | console.log("Transaction validated"); 203 | 204 | // Create the charge options 205 | const chargeOptions = { 206 | source: token.id, 207 | currency: 'usd', 208 | description: `NFT Purchase - ${tokenIdString}`, 209 | amount: 100, 210 | capture: false 211 | }; 212 | 213 | // Authorize the card without charging (capture: false) 214 | return stripe.charges.create(chargeOptions).then(charge => { 215 | console.log("Created initial credit card authorization"); 216 | 217 | // If the charge is valid, submit the ethereum transaction 218 | return transaction.submit(this.currentAccount).then((transactionHash) => { 219 | console.log(`Ethereum transaction submitted ${transactionHash}`); 220 | 221 | // Update the charge with the transaction hash for accounting later 222 | stripe.charges.update(charge.id, { 223 | metadata: { 224 | transactionHash: transactionHash 225 | } 226 | }).then(() => { 227 | console.log('Charge was updated with metadata'); 228 | }).catch(error => { 229 | console.log('Error updating charge metadata'); 230 | console.error(error); 231 | }); 232 | 233 | // Add an event listener for the transaction to be confirmed 234 | transaction.once('confirmed', () => { 235 | stripe.charges.capture(charge.id).then(capture => { 236 | console.log('Charge captured'); 237 | }).catch(error => { 238 | console.log('Error capturing charge'); 239 | console.error(error); 240 | }); 241 | }); 242 | return { transactionHash, token: { id: tokenIdString, imageId: expectedImageId } }; 243 | }); 244 | }); 245 | }); 246 | } 247 | 248 | processAppleTransaction(transactionId, receipt, recipient) { 249 | const {transaction, tokenIdString, expectedImageId} = this.createMintTransaction(recipient); 250 | 251 | return transaction.validate(this.currentAccount).then(() => { 252 | console.log("Transaction validated"); 253 | let body = { 254 | 'receipt-data': receipt, 255 | 'password:': process.env['APPSTORE_PASSWORD'], 256 | } 257 | return fetch('https://sandbox.itunes.apple.com/verifyReceipt', { 258 | method: 'post', 259 | body: JSON.stringify(body), 260 | headers: { 'Content-Type': 'application/json' }, 261 | }) 262 | .then(res => { 263 | if (res.ok) { // res.status >= 200 && res.status < 300 264 | return res; 265 | } else { 266 | throw new Error('Could not communicate with the app store'); 267 | } 268 | }) 269 | .then(res => res.json()) 270 | .then(appStoreResponse => { 271 | if (appStoreResponse['status'] == 0) { 272 | return appStoreResponse['receipt']; 273 | } else { 274 | throw new Error(`Got an invalid receipt status: ${appStoreResponse['status']}`); 275 | } 276 | }) 277 | .then(receipt => { 278 | const matchingTransaction = receipt.in_app.find(item => { 279 | return (item.transaction_id || item.original_transaction_id) === transactionId 280 | }); 281 | if (!matchingTransaction) { 282 | throw new Error('No matching transaction in receipt'); 283 | } 284 | console.log('Got receipt from apple', matchingTransaction); 285 | 286 | // TODO: Verify that this receipt has only been used once. 287 | 288 | return transaction.submit(this.currentAccount).then((transactionHash) => { 289 | return { transactionHash, token: { id: tokenIdString, imageId: expectedImageId } }; 290 | }); 291 | }) 292 | }); 293 | } 294 | 295 | getConfig() { 296 | return { 297 | networkId: this.networkId, 298 | contractAddress: this.contract.options.address, 299 | tokenAddress: this.token.options.address, 300 | address: this.currentAccount, 301 | balance: this.balance, 302 | name: this.name 303 | } 304 | } 305 | } 306 | 307 | module.exports = App; 308 | -------------------------------------------------------------------------------- /server/app/contract.js: -------------------------------------------------------------------------------- 1 | // This script bridges a Solidity contract that was compiled with Truffle 2 | // into a Web3 contract we can use in the app. See how it's used in app.js. 3 | 4 | class Contract { 5 | 6 | constructor(web3, networkId, artifacts) { 7 | this.web3 = web3; 8 | this.networkId = networkId; 9 | this.artifacts = artifacts; 10 | } 11 | 12 | /** 13 | * Use this to create an instance of the contract at the given address. 14 | * ``` 15 | * const MyContract = Contract(web3, artifacts); 16 | * const instance = MyContract.at('0x00000…'); 17 | * ``` 18 | * @param {string} address Ethereum address the contract is deployed at 19 | * @returns {Object} An instance of web3.eth.Contract pointing at the address given. 20 | */ 21 | at(address) { 22 | if (this.artifacts && this.artifacts.abi) { 23 | const abi = this.artifacts.abi; 24 | return new this.web3.eth.Contract(abi, address); 25 | } else { 26 | throw new Error('Contract not compiled or not found'); 27 | } 28 | } 29 | 30 | /** 31 | * Get an instance of this contract deployed on the current network. 32 | * ``` 33 | * const MyContract = Contract(web3, artifacts); 34 | * MyContract.deployed().then(instance => { 35 | * this.contractInstance = instance; 36 | * }); 37 | * ``` 38 | * @returns {Promise} A Promise containing an instance of the contract. 39 | */ 40 | deployed() { 41 | const address = this.getAddress(this.networkId); 42 | if (address) { 43 | return Promise.resolve(this.at(address)); 44 | } else { 45 | const deployedNetworks = Object.keys(this.artifacts.networks); 46 | return Promise.reject(`Contract not deployed on current network (${this.networkId}). Make sure you ran truffle migrate for the network this environment points to. Currently deployed on: ${deployedNetworks}.`); 47 | } 48 | } 49 | 50 | /** 51 | * Searches the artifacts to get the address at a given network id 52 | * @param {string} networkID numeric id string of the ethereum network to get the address for. eg "1" for main net. 53 | */ 54 | getAddress(networkID) { 55 | if (this.artifacts && this.artifacts.networks && this.artifacts.networks[networkID] && this.artifacts.networks[networkID].address) { 56 | return this.artifacts.networks[networkID].address; 57 | } 58 | } 59 | } 60 | 61 | module.exports = Contract; 62 | -------------------------------------------------------------------------------- /server/app/server.js: -------------------------------------------------------------------------------- 1 | const Express = require('express'); 2 | const cors = require('cors'); 3 | const bodyParser = require('body-parser'); 4 | const Sentry = require('@sentry/node'); 5 | 6 | class Server { 7 | 8 | constructor(port, app) { 9 | this.app = app; 10 | this.server = Express(); 11 | this.configureMiddleware(); 12 | this.defineRoutes(); 13 | if (process.env.SENTRY_DSN) { 14 | this.server.use(Sentry.Handlers.errorHandler()); 15 | } 16 | this.start(port); 17 | } 18 | 19 | configureMiddleware() { 20 | if (process.env.SENTRY_DSN) { 21 | this.server.use(Sentry.Handlers.requestHandler()); 22 | } 23 | // Allow CORS 24 | this.server.use(cors()); 25 | 26 | // Parse JSON requests 27 | this.server.use(bodyParser.json()); 28 | } 29 | 30 | defineRoutes() { 31 | // Returns some metadata and health information. You could use this to consume the contract 32 | // address in your web app for example, or use a monitoring service to ensure sufficient balance. 33 | this.server.get('/', (req, res) => { 34 | const config = this.app.getConfig(); 35 | res.json(config); 36 | }); 37 | 38 | // ===================== 39 | // Contract State 40 | // ===================== 41 | 42 | // Returns the total supply (total number of tokens) 43 | this.server.get('/totalSupply', (req, res) => { 44 | this.app.getTotalSupply().then(totalSupply => { 45 | res.json({ totalSupply }); 46 | }).catch(error => { 47 | res.status(500).json({ error: error.toString() }); 48 | }); 49 | }); 50 | 51 | // Returns the name of the contract. Not really that useful :) 52 | this.server.get('/name', (req, res) => { 53 | this.app.getName().then(name => { 54 | res.json({ name }); 55 | }).catch(error => { 56 | res.status(500).json({ error: error.toString() }); 57 | }); 58 | }); 59 | 60 | // Returns the mint limit directly from the contract 61 | // (the arbitrary maximum number of tokens per address) 62 | this.server.get('/mintLimit', (req, res) => { 63 | this.app.getMintLimit().then(mintLimit => { 64 | res.json({ mintLimit }); 65 | }).catch(error => { 66 | res.status(500).json({ error: error.toString() }); 67 | }); 68 | }); 69 | 70 | // Returns the symbol of the contact (part of the ERC721 standard) 71 | this.server.get('/symbol', (req, res) => { 72 | this.app.getSymbol().then(symbol => { 73 | res.json({ symbol }); 74 | }).catch(error => { 75 | res.status(500).json({ error: error.toString() }); 76 | }); 77 | }); 78 | 79 | // ===================== 80 | // Enumerating Tokens 81 | // ===================== 82 | 83 | // Returns all token ids that belong to the provided address. 84 | // You could use something like this to load data on your client 85 | // in a more standard JSON format, rather than dealing with web3. 86 | this.server.get('/:ownerAddress/tokens', (req, res) => { 87 | this.app.getTokens(req.params.ownerAddress).then(tokens => { 88 | res.json({ tokens }); 89 | }).catch(error => { 90 | res.status(500).json({ error: error.toString() }); 91 | }); 92 | }); 93 | 94 | // Returns the token balance of the provided address. 95 | this.server.get('/:ownerAddress/balance', (req, res) => { 96 | this.app.getBalance(req.params.ownerAddress).then(balance => { 97 | res.json({ balance }); 98 | }).catch(error => { 99 | res.status(500).json({ error: error.toString() }); 100 | }); 101 | }); 102 | 103 | // ===================== 104 | // Token Metadata 105 | // ===================== 106 | 107 | // An important part of NFTs is showing the characteristics of the token. 108 | // The ERC-721 spec includes a method for getting a web URI that includes the 109 | // details of the token in a JSON format. Our backend app can not only host that end-point 110 | // but load some of the metadata from the contract itself, completing the loop. 111 | this.server.get('/tokens/:tokenId', (req, res) => { 112 | if (!req.params.tokenId || !req.params.tokenId.match(/^\d+$/g)) { 113 | return res.send({ error: { message: 'Invalid token id passed' } }); 114 | } 115 | this.app.getTokenMetadata(req.params.tokenId).then(metadata => { 116 | res.json(metadata); 117 | }).catch(error => { 118 | res.status(500).json({ error: error.toString() }); 119 | }); 120 | }); 121 | 122 | // Returns the tokenURI for a given token ID from the contract 123 | this.server.get('/tokenURI/:tokenId', (req, res) => { 124 | this.app.getTokenURI(req.params.tokenId).then((uri) => { 125 | res.json({ tokenURI: uri }); 126 | }).catch(error => { 127 | res.status(500).json({ error: error.toString() }); 128 | }); 129 | }); 130 | 131 | // ================================ 132 | // Transactions with App Wallet 133 | // ================================ 134 | 135 | this.server.post('/process-transaction', (req, res) => { 136 | const { transactionIdentifier, receipt, token, recipient } = req.body; 137 | 138 | // Ensure recipient was submitted 139 | if (!recipient) { 140 | res.status(422).json({ 141 | error: { 142 | message: 'Recipient address not provided' 143 | } 144 | }); 145 | return; 146 | } 147 | 148 | if (token) { 149 | this.app.processStripeTransaction(token, recipient).then((response) => { 150 | res.json(response); 151 | }).catch(error => { 152 | console.error(error); 153 | res.status(500).json({ 154 | error: { 155 | message: error.toString() 156 | } 157 | }); 158 | }); 159 | } else if (receipt) { 160 | this.app.processAppleTransaction(transactionIdentifier, receipt, recipient).then((response) => { 161 | res.json(response); 162 | }).catch(error => { 163 | console.error(error); 164 | res.status(500).json({ 165 | error: { 166 | message: error.toString() 167 | } 168 | }); 169 | }); 170 | } else { 171 | res.status(422).json({ 172 | error: { 173 | message: 'Transaction token or Apple receipt not submitted' 174 | } 175 | }); 176 | } 177 | }); 178 | } 179 | 180 | start(port) { 181 | // Start server 182 | this.server.listen(port, () => console.log(`Listening on port ${port}!`)); 183 | } 184 | 185 | } 186 | 187 | module.exports = Server; 188 | -------------------------------------------------------------------------------- /server/app/transaction.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require("events"); 2 | 3 | const MIN_CONFIRMATIONS = 2; 4 | 5 | class Transaction extends EventEmitter { 6 | 7 | constructor(web3, method) { 8 | super(); 9 | this.web3 = web3; 10 | this.method = method; 11 | } 12 | 13 | get isConfirmed() { 14 | return this._isConfirmed; 15 | } 16 | 17 | set isConfirmed(value) { 18 | if (this._isConfirmed !== value) { 19 | this._isConfirmed = value; 20 | if (value === true) { 21 | this.submittedTransaction.removeAllListeners(); 22 | this.emit('confirmed'); 23 | } 24 | } 25 | } 26 | 27 | get error() { 28 | return this._error; 29 | } 30 | 31 | set error(value) { 32 | this._error = value; 33 | if (value) { 34 | this.emit('error', value); 35 | } 36 | } 37 | 38 | get receipt() { 39 | return this._receipt; 40 | } 41 | 42 | set receipt(value) { 43 | this._receipt = value; 44 | } 45 | 46 | setInputs() { 47 | this.inputs = [...arguments]; 48 | this.transaction = this.method(...arguments); 49 | } 50 | 51 | validate(from) { 52 | return this.transaction.estimateGas({ from: from }).then(estimatedGas => { 53 | this.gas = estimatedGas; 54 | return this.web3.eth.getGasPrice().then(estimatedGasPrice => { 55 | this.gasPrice = estimatedGasPrice; 56 | return this.web3.eth.getBalance(from).then(currentBalance => { 57 | const balance = this.web3.utils.toBN(currentBalance); 58 | const gasPrice = this.web3.utils.toBN(estimatedGasPrice); 59 | const gas = this.web3.utils.toBN(estimatedGas); 60 | const maxGas = gas.mul(gasPrice); 61 | if (balance.lt(maxGas)) { 62 | throw new Error('Insufficient funds. Add more ETH to your wallet and try again.'); 63 | } else { 64 | return true; 65 | } 66 | }); 67 | }); 68 | }); 69 | } 70 | 71 | submit(from) { 72 | if (!this.gas || !this.gasPrice) { 73 | return Promise.reject(new Error('Transaction not yet validated. Please call validate() and try again')); 74 | } 75 | return new Promise((fulfill, reject) => { 76 | this.submittedTransaction = this.transaction.send({ from: from, gas: this.gas, gasPrice: this.gasPrice }, (error, transactionHash) => { 77 | if (error) { 78 | reject(error); 79 | return; 80 | } else { 81 | fulfill(transactionHash); 82 | } 83 | }); 84 | this.submittedTransaction.on('transactionHash', (hash) => { 85 | this.processTransactionHash(hash); 86 | }); 87 | this.submittedTransaction.on('error', (error) => { 88 | this.processTransactionError(error); 89 | }); 90 | this.submittedTransaction.on('confirmation', (confirmationCount, receipt) => { 91 | this.processConfirmation(confirmationCount, receipt); 92 | }); 93 | }); 94 | } 95 | 96 | processTransactionHash(hash) { 97 | this.transactionHash = hash; 98 | } 99 | 100 | processTransactionError(error) { 101 | this.error = error; 102 | } 103 | 104 | processConfirmation(confirmationCount, receipt) { 105 | this.receipt = receipt; 106 | if (confirmationCount >= MIN_CONFIRMATIONS) { 107 | this.isConfirmed = true; 108 | } 109 | } 110 | 111 | confirm() { 112 | if (this.isConfirmed) { 113 | return Promise.resolve(); 114 | } 115 | return new Promise((fulfill, reject) => { 116 | this.once('confirmed', () => { 117 | fulfill(); 118 | }); 119 | }); 120 | } 121 | 122 | } 123 | 124 | module.exports = Transaction; 125 | -------------------------------------------------------------------------------- /server/bitski.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | app: { 3 | id: '365c9c6a-5602-4bc6-942c-7084beada709' //change this to your app's client id 4 | }, 5 | appWallet: { 6 | client: { 7 | //if you have an app wallet, add your client id and secret here 8 | id: 'ad2c4eb9-bc22-4000-b17e-54dd1e568440', 9 | secret: '56iBfVL]fxB-pRyS}19[er9lBxuD}MvcUV6P0Yl7UE]pNjJR}ntkVkC-Iysg6raDt' 10 | }, 11 | auth: { 12 | tokenHost: 'https://account.bitski.com', 13 | tokenPath: '/oauth2/token' 14 | } 15 | }, 16 | environments: { 17 | development: { 18 | network: 'development', //ethereum network to use for local dev 19 | redirectURL: 'http://localhost:3000/callback.html' //url the popup will redirect to when logged in 20 | }, 21 | production: { 22 | network: 'kovan', //ethereum network to use for production 23 | redirectURL: 'https://mydomain.com/callback.html' //url the popup will redirect to when logged in 24 | } 25 | }, 26 | networkIds: { 27 | kovan: 'kovan', 28 | rinkeby: 'rinkeby', 29 | live: 'mainnet', 30 | development: 'http://localhost:9545' //Update this if you use Ganache or another local blockchain 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const Sentry = require('@sentry/node'); 3 | const App = require('./app/app'); 4 | 5 | if (process.env.SENTRY_DSN) { 6 | Sentry.init({ dsn: process.env.SENTRY_DSN }); 7 | } 8 | 9 | const credentials = { 10 | id: process.env.BITSKI_APP_WALLET_ID, 11 | secret: process.env.BITSKI_APP_WALLET_SECRET 12 | }; 13 | 14 | const app = new App(credentials.id, 'rinkeby', credentials); 15 | app.start(process.env.PORT || 4200); 16 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-demo-api", 3 | "version": "1.0.0", 4 | "description": "Backend app that is responsible for charging cards and creating tokens", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@sentry/node": "^4.6.2", 14 | "bitski-node": "^0.7.1", 15 | "body-parser": "^1.18.3", 16 | "cors": "^2.8.4", 17 | "dotenv": "^6.1.0", 18 | "express": "^4.16.3", 19 | "stripe": "^6.20.0", 20 | "web3": "^1.0.0-beta.55" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | var ExampleApp = artifacts.require("ExampleApp"); 2 | var Token = artifacts.require("LimitedMintableNonFungibleToken"); 3 | 4 | contract('ExampleApp', (accounts) => { 5 | 6 | it('should not be able to mint if not from app wallet', () => { 7 | const tokenId = web3.utils.randomHex(32); 8 | const tokenIdString = web3.utils.hexToNumberString(tokenId); 9 | const tokenURI = `https://foo.bar/tokens/${tokenIdString}`; 10 | 11 | return ExampleApp.deployed().then(instance => { 12 | return instance.mint(accounts[0], tokenId, tokenURI, { from: accounts[1] }).catch(error => { 13 | assert.ok(error); 14 | }); 15 | }); 16 | }); 17 | 18 | 19 | it('should be able to mint a token', () => { 20 | const tokenId = web3.utils.randomHex(32); 21 | const tokenIdString = web3.utils.hexToNumberString(tokenId); 22 | const tokenURI = `https://foo.bar/tokens/${tokenIdString}`; 23 | 24 | return ExampleApp.deployed().then(instance => { 25 | return instance.mint(accounts[0], tokenId, tokenURI); 26 | }).then(result => { 27 | return Token.deployed().then(tokenInstance => { 28 | return tokenInstance.ownerOf(tokenId); 29 | }).then(result => { 30 | assert.equal(result.valueOf(), accounts[0]); 31 | }); 32 | }); 33 | }); 34 | 35 | it('should not be able to update the address if not the app wallet', () => { 36 | return ExampleApp.deployed().then(instance => { 37 | return instance.setAppWalletAddress(accounts[1], { from: accounts[1] }).catch(error => { 38 | assert.ok(error); 39 | }); 40 | }); 41 | }); 42 | 43 | it('should be able to update the app wallet address', () => { 44 | const tokenId = web3.utils.randomHex(32); 45 | const tokenIdString = web3.utils.hexToNumberString(tokenId); 46 | const tokenURI = `https://foo.bar/tokens/${tokenIdString}`; 47 | 48 | return ExampleApp.deployed().then(instance => { 49 | return instance.setAppWalletAddress(accounts[1], { from: accounts[0] }).then(() => { 50 | return instance.mint(accounts[0], tokenId, tokenURI, { from: accounts[1] }); 51 | }).then(result => { 52 | assert.ok(result); 53 | }); 54 | }); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /test/nft.js: -------------------------------------------------------------------------------- 1 | // Specifically request an abstraction for MetaCoin 2 | var LimitedMintableNonFungibleToken = artifacts.require("LimitedMintableNonFungibleToken"); 3 | 4 | contract('LimitedMintableNonFungibleToken', function(accounts) { 5 | it("should implement ERC721 interface", function(){ 6 | return LimitedMintableNonFungibleToken.deployed().then(function(instance) { 7 | return instance.supportsInterface('0x80ac58cd'); 8 | }).then(result => { 9 | assert.equal(result.valueOf(), true, "Doesn't implement ERC721"); 10 | }); 11 | }); 12 | 13 | it("should be able to mint", function() { 14 | return LimitedMintableNonFungibleToken.deployed().then(function(instance) { 15 | return instance.isMinter(accounts[0]).then(result => { 16 | assert.equal(result.valueOf(), true, 'default account should be allowed to mint'); 17 | return instance.mint(accounts[0], 1, { from: accounts[0] }); 18 | }).then(function(result) { 19 | return instance.balanceOf(accounts[0]); 20 | }).then(function(balance) { 21 | assert.equal(balance.valueOf(), 1, "Token should be in the account"); 22 | }); 23 | }); 24 | }); 25 | 26 | it("should be able to mint with token uri", function() { 27 | return LimitedMintableNonFungibleToken.deployed().then(function(instance) { 28 | return instance.mintWithTokenURI(accounts[0], 2, "https://foo.bar", {from: accounts[0]}).then(function(result) { 29 | return instance.tokenURI(2); 30 | }).then(function(tokenURI) { 31 | assert.equal(tokenURI.valueOf(), "https://foo.bar", "Token should have correct URI"); 32 | }); 33 | }); 34 | }); 35 | 36 | it("should not be able to mint if not a minter", function() { 37 | return LimitedMintableNonFungibleToken.deployed().then(function(instance) { 38 | return instance.mint(accounts[1], 3, { from: accounts[1] }).catch(function(error){ 39 | assert.ok(error, "Should have returned an error"); 40 | return instance.balanceOf(accounts[1]); 41 | }).then(function(balance) { 42 | assert.equal(balance.valueOf(), 0, "Second account should not have any tokens"); 43 | }); 44 | }); 45 | }); 46 | 47 | it("should be able to add minter", function() { 48 | return LimitedMintableNonFungibleToken.deployed().then(instance => { 49 | return instance.addMinter(accounts[1], { from: accounts[0] }).then(() => { 50 | return instance.mint(accounts[0], 3, { from: accounts[1] }); 51 | }).then(result => { 52 | return instance.balanceOf(accounts[0]); 53 | }).then(result => { 54 | assert.equal(result.valueOf(), 3, "Should have minted token"); 55 | }); 56 | }); 57 | }); 58 | 59 | it("should be able to list tokens", function() { 60 | return LimitedMintableNonFungibleToken.deployed().then(function(instance) { 61 | return instance.getOwnerTokens(accounts[0]).then(tokens => { 62 | assert.ok(tokens.valueOf(), "should have returned a value"); 63 | assert.equal(tokens.valueOf().length, 3, "should have 2 token ids"); 64 | }); 65 | }); 66 | }); 67 | 68 | it("should not error when listing tokens for empty balance", function() { 69 | return LimitedMintableNonFungibleToken.deployed().then(function(instance) { 70 | return instance.getOwnerTokens(accounts[1]).then(tokens => { 71 | assert.ok(tokens.valueOf(), "should have returned a value"); 72 | assert.equal(tokens.valueOf().length, 0, "should have 2 token ids"); 73 | }); 74 | }); 75 | }); 76 | 77 | it("should properly return image id", function() { 78 | return LimitedMintableNonFungibleToken.deployed().then(function(instance) { 79 | return instance.mint(accounts[0], 9, { from: accounts[0] }).then(result => { 80 | return instance.imageId(9); 81 | }).then(imageId => { 82 | assert.equal(imageId.valueOf(), 5, "imageId should return proper value"); 83 | }); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { ProviderManager } = require('bitski-node'); 3 | 4 | const id = process.env.BITSKI_APP_WALLET_ID; 5 | const secret = process.env.BITSKI_APP_WALLET_SECRET; 6 | 7 | const providerManager = new ProviderManager(id, secret); 8 | 9 | module.exports = { 10 | networks: { 11 | development: { 12 | host: "localhost", 13 | port: 9545, 14 | network_id: "*", 15 | gas: 6700000 16 | }, 17 | live: { 18 | network_id: '1', 19 | provider: () => { 20 | return providerManager.getProvider('mainnet'); 21 | } 22 | }, 23 | kovan: { 24 | network_id: '42', 25 | provider: () => { 26 | return providerManager.getProvider('kovan'); 27 | } 28 | }, 29 | rinkeby: { 30 | network_id: '4', 31 | provider: () => { 32 | return providerManager.getProvider('rinkeby'); 33 | }, 34 | gas: 4000000 35 | } 36 | } 37 | }; 38 | --------------------------------------------------------------------------------