├── test ├── .gitignore └── KittyCoreTest.js ├── .eslintignore ├── box-img-lg.png ├── box-img-sm.png ├── scripts ├── help.js ├── import-bug-cat.js ├── mine-blocks.js ├── run-script.js └── setup.js ├── .gitignore ├── truffle.js ├── api ├── db.js ├── index.js ├── static │ ├── dashboard.js │ ├── dashboard.html │ ├── axios.min.js │ └── vue.min.js ├── routes-cryptokitties.js └── routes-cheshire.js ├── contracts ├── Migrations.sol ├── GeneScience.sol ├── SiringClockAuction.sol └── SaleClockAuction.sol ├── .eslintrc.json ├── truffle-box.json ├── LICENSE ├── .circleci └── config.yml ├── package.json ├── migrations └── 1_initial_migration.js ├── config.json ├── lib ├── contract.js ├── user.js ├── cheshire.js └── kitty.js ├── test-cheshire ├── contract.js ├── routes-cryptokitties.js ├── routes-cheshire.js ├── cheshire.js ├── user.js └── kitty.js ├── server.js └── README.md /test/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | api/static 3 | migrations 4 | -------------------------------------------------------------------------------- /box-img-lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/endless-nameless-inc/cheshire/HEAD/box-img-lg.png -------------------------------------------------------------------------------- /box-img-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/endless-nameless-inc/cheshire/HEAD/box-img-sm.png -------------------------------------------------------------------------------- /scripts/help.js: -------------------------------------------------------------------------------- 1 | // yarn run help 2 | 3 | module.exports = async function setup(cheshire) { 4 | cheshire.printHelp() 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | node_modules 4 | scripts/call-race.js 5 | scripts/call-register-for-race.js 6 | scripts/give-me-kitties.js 7 | scripts/setup-kitty-race.js 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /scripts/import-bug-cat.js: -------------------------------------------------------------------------------- 1 | module.exports = async function importBugCat(cheshire) { 2 | const bugCatIdMainnet = 101 3 | const ownerTestnet = cheshire.accounts[0].address 4 | const kittyIdTestnet = await cheshire.importKitty(bugCatIdMainnet, ownerTestnet) 5 | 6 | console.log(`Kitty #${kittyIdTestnet} => ${ownerTestnet}`) 7 | } 8 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | const config = require('./config.json') 2 | 3 | module.exports = { 4 | // See 5 | // to customize your Truffle configuration! 6 | networks: { 7 | cheshire: { 8 | host: 'localhost', 9 | port: config.portTestnet, 10 | network_id: 1337, 11 | }, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /api/db.js: -------------------------------------------------------------------------------- 1 | const sqlite3 = require('sqlite3') 2 | 3 | const db = new sqlite3.Database(':memory:') 4 | 5 | db.serialize(() => { 6 | db.run('CREATE TABLE kitties (id_mainnet integer, id_testnet integer, owner string, api_object BLOB)') 7 | db.run('CREATE TABLE users (address_mainnet string, address_testnet string, api_object BLOB)') 8 | }) 9 | 10 | module.exports = db 11 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 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 | -------------------------------------------------------------------------------- /contracts/GeneScience.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | contract GeneScience { 4 | /// @dev simply a boolean to indicate this is the contract we expect to be 5 | function isGeneScience() public pure returns (bool) { 6 | return true; 7 | } 8 | 9 | /// @dev given genes of kitten 1 & 2, return a genetic combination - may have a random factor 10 | /// @param genes1 genes of mom 11 | /// @param genes2 genes of sire 12 | /// @return the genes that are supposed to be passed down the child 13 | function mixGenes(uint256 genes1, uint256 genes2, uint256 targetBlock) public returns (uint256) { 14 | return genes1; // MVP 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true, 6 | "mocha": true 7 | }, 8 | "extends": "airbnb/base", 9 | "parserOptions": { 10 | "sourceType": "module", 11 | "ecmaVersion": 2017 12 | }, 13 | "rules": { 14 | "indent": [ 15 | "error", 16 | 2 17 | ], 18 | "linebreak-style": [ 19 | "error", 20 | "unix" 21 | ], 22 | "no-console": "off", 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "never" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /truffle-box.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | ".eslintignore", 4 | ".eslintrc.json", 5 | ".gitignore", 6 | "LICENSE", 7 | "README.md", 8 | "box-img-lg-template.png", 9 | "box-img-sm-template.png", 10 | "test-cheshire" 11 | ], 12 | "commands": { 13 | "Start Cheshire": "yarn start", 14 | "Open Cheshire dashboard": "yarn run dashboard", 15 | "Run Cheshire script": "yarn run script ./scripts/yourscript.js", 16 | "Mine blocks": "yarn run mine ", 17 | "Help / System status": "yarn run help", 18 | "Compile contracts": "truffle compile", 19 | "Migrate contracts": "truffle migrate", 20 | "Test contracts": "truffle test" 21 | }, 22 | "hooks": { 23 | "post-unpack": "yarn install" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | // Serves CryptoKitties API and internal Cheshire API 2 | const bodyParser = require('koa-bodyparser') 3 | const cors = require('@koa/cors') 4 | const Koa = require('koa') 5 | const mount = require('koa-mount') 6 | const path = require('path') 7 | const serve = require('koa-static') 8 | const views = require('koa-views') 9 | 10 | const routesCryptoKitties = require('./routes-cryptokitties.js') 11 | const routesCheshire = require('./routes-cheshire.js') 12 | 13 | module.exports = new Koa() 14 | .use(cors({ origin: '*' })) 15 | .use(bodyParser()) 16 | .use(views(path.join(__dirname, '/views'), { extension: 'ejs' })) 17 | .use(mount('/static', serve('./api/static'))) 18 | .use(routesCheshire.routes()) 19 | .use(routesCheshire.allowedMethods()) 20 | .use(routesCryptoKitties.routes()) 21 | .use(routesCryptoKitties.allowedMethods()) 22 | -------------------------------------------------------------------------------- /scripts/mine-blocks.js: -------------------------------------------------------------------------------- 1 | // yarn run mine 2 | 3 | const Web3 = require('web3') 4 | const config = require('../config.json') 5 | 6 | const { log } = console 7 | 8 | module.exports = async function mineBlocks() { 9 | const mineBlock = () => new Promise((resolve, reject) => { 10 | new Web3.providers.HttpProvider(`http://localhost:${config.portTestnet}`).sendAsync({ 11 | jsonrpc: '2.0', 12 | method: 'evm_mine', 13 | id: 12345, 14 | }, (err, result) => { 15 | if (err) { 16 | return reject(err) 17 | } 18 | return resolve(result) 19 | }) 20 | }) 21 | 22 | let numBlocks = parseInt(process.argv.slice().pop(), 10) 23 | if (Number.isNaN(numBlocks) || numBlocks < 1) { 24 | numBlocks = 1 25 | } 26 | 27 | log(`Mining ${numBlocks} blocks...`) 28 | 29 | const promises = [] 30 | for (let i = 0; i < numBlocks; i += 1) { 31 | promises.push(mineBlock()) 32 | } 33 | await Promise.all(promises) 34 | 35 | log('Done') 36 | } 37 | -------------------------------------------------------------------------------- /api/static/dashboard.js: -------------------------------------------------------------------------------- 1 | 2 | new Vue({ 3 | el: '#cheshire-dashboard', 4 | data: { 5 | colorMap: { 6 | babyblue: '#4eb4f9', 7 | babypuke: '#bcba5e', 8 | bubblegum: '#fbe0f4', 9 | chestnut: '#efe1db', 10 | coral: '#45f0f4', 11 | gold: '#faf5cf', 12 | limegreen: '#d9f5cc', 13 | mintgreen: '#d9f5cc', 14 | pumpkin: '#ffa039', 15 | sizzurp: '#dfe0fa', 16 | strawberry: '#fcdedf', 17 | thundergrey: '#828282', 18 | topaz: '#d1eeeb', 19 | violet: '#ba8aff', 20 | }, 21 | contracts: {}, 22 | kitties: [], 23 | users: [], 24 | }, 25 | 26 | async mounted () { 27 | this.contracts = (await axios.get('/cheshire/contracts')).data 28 | this.kitties = (await axios.get('/kitties?limit=-1')).data.kitties.map((kitty) => { 29 | kitty.showAttrs = false 30 | return kitty 31 | }) 32 | this.users = (await axios.get('/cheshire/users')).data.users 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Endless Nameless Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:8.11-browsers 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/cheshire 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test-ci 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /api/routes-cryptokitties.js: -------------------------------------------------------------------------------- 1 | // Routes in this file map to the public CryptoKitties API. 2 | const Router = require('koa-router') 3 | 4 | const Kitty = require('../lib/kitty.js') 5 | const User = require('../lib/user.js') 6 | 7 | module.exports = new Router() 8 | .get('/kitties/:id', async (ctx) => { 9 | const kitty = await Kitty.findById(parseInt(ctx.params.id, 10)) 10 | 11 | if (kitty === undefined) { 12 | ctx.status = 404 13 | } else { 14 | ctx.body = JSON.parse(kitty.api_object) 15 | } 16 | }) 17 | .get('/kitties', async (ctx) => { 18 | const limit = ctx.query.limit ? parseInt(ctx.query.limit, 10) : 12 19 | const offset = ctx.query.offset ? parseInt(ctx.query.offset, 10) : 0 20 | const kitties = (await Kitty.findAll(ctx.query.owner_wallet_address, limit, offset)) 21 | .map(row => JSON.parse(row.api_object)) 22 | const total = await Kitty.count(ctx.query.owner_wallet_address) 23 | 24 | ctx.body = { 25 | limit, 26 | offset, 27 | kitties, 28 | total, 29 | } 30 | }) 31 | .get('/user/:address', async (ctx) => { 32 | const user = await User.findByAddress(ctx.params.address) 33 | 34 | if (user === undefined) { 35 | ctx.status = 404 36 | ctx.body = { error: 'user not found' } 37 | } else { 38 | ctx.body = JSON.parse(user.api_object) 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /scripts/run-script.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const axios = require('axios') 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const config = require('../config.json') 7 | const Cheshire = require('../lib/cheshire.js') 8 | 9 | const { log } = console 10 | 11 | async function setEnvironmentVariables() { 12 | try { 13 | const contracts = (await axios.get(`http://localhost:${config.portApi}/cheshire/contracts`)).data 14 | 15 | _.forOwn(contracts, (contractAddress, contractName) => { 16 | process.env[`ADDRESS_${_.snakeCase(contractName).toUpperCase()}`] = contractAddress 17 | }) 18 | } catch (e) { 19 | if (e.code === 'ECONNREFUSED') { 20 | log(` 21 | ** NOTE: Cheshire API server not running. Could not set contract address environment variables 22 | `) 23 | } else { 24 | throw e 25 | } 26 | } 27 | } 28 | 29 | async function run() { 30 | const scriptPath = path.join(process.cwd(), process.argv[2]) 31 | if (!fs.existsSync(scriptPath)) { 32 | log(`ERROR: the file ${scriptPath} does not exist`) 33 | process.exit(1) 34 | } 35 | 36 | await setEnvironmentVariables() 37 | const cheshire = new Cheshire(config) 38 | 39 | // eslint-disable-next-line global-require, import/no-dynamic-require 40 | require(scriptPath)(cheshire, ...process.argv.slice(3)) 41 | } 42 | 43 | run() 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@koa/cors": "2", 4 | "axios": "^0.18.0", 5 | "coveralls": "^3.0.1", 6 | "ejs": "^2.6.1", 7 | "eslint": "^4.19.1", 8 | "eslint-config-airbnb": "^16.1.0", 9 | "eslint-plugin-import": "^2.12.0", 10 | "ganache-cli": "^6.1.0", 11 | "jest": "^23.0.0", 12 | "koa": "^2.5.1", 13 | "koa-bodyparser": "^4.2.0", 14 | "koa-mount": "^3.0.0", 15 | "koa-router": "^7.4.0", 16 | "koa-static": "^4.0.3", 17 | "koa-views": "^6.1.4", 18 | "lodash": "^4.17.10", 19 | "sqlite3": "^4.0.0", 20 | "supertest": "^3.1.0", 21 | "truffle": "^4.1.11", 22 | "truffle-contract": "^3.0.5", 23 | "web3": "v0.20.6" 24 | }, 25 | "scripts": { 26 | "help": "node ./scripts/run-script.js ./scripts/help.js", 27 | "dashboard": "open http://localhost:4000", 28 | "mine": "truffle --network cheshire exec scripts/mine-blocks.js", 29 | "script": "node ./scripts/run-script.js", 30 | "eslint": "eslint .", 31 | "postinstall": "truffle compile", 32 | "test": "jest --coverage", 33 | "test-ci": "jest --coverage --coverageReporters=text-lcov | coveralls" 34 | }, 35 | "jest": { 36 | "testMatch": [ 37 | "**/test-cheshire/*.js" 38 | ], 39 | "coverageReporters": [ 40 | "text-summary", 41 | "lcov" 42 | ], 43 | "bail": true, 44 | "restoreMocks": true, 45 | "testEnvironment": "node" 46 | }, 47 | "license": "MIT" 48 | } 49 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | // `yarn start` runs setup.js by default. Use this script to start your dApp 2 | // and seed it with test data. 3 | const { exec } = require('child_process') 4 | 5 | const { log } = console 6 | 7 | module.exports = async function setup(cheshire) { 8 | // Deploy contract... 9 | // const myContract = await cheshire.deployContract('MyContract') 10 | // log('MyContract deployed at:', myContract.address) 11 | 12 | // Fetch Genesis kitty... 13 | const kittyIdGenesis = 1 14 | await cheshire.importKitty(kittyIdGenesis, cheshire.accounts[9].address) 15 | log(`Genesis kitty assigned to ${cheshire.accounts[9].address}`) 16 | 17 | // Start dApp... 18 | if (process.env.APP_START) { 19 | log('Running:', process.env.APP_START) 20 | log('====================') 21 | const child = exec(process.env.APP_START) 22 | child.stdout.pipe(process.stdout) 23 | child.stderr.pipe(process.stderr) 24 | child.on('error', log) 25 | child.on('close', code => log('Exited with code', code)) 26 | } else { 27 | log(` 28 | *** NOTE: you can pass \`yarn start\` a command to start your app. We 29 | recommend you do this so your processes inherit the various environment 30 | variables set by Cheshire. For example: 31 | 32 | APP_START="cd ~/Projects/kittyrace-web; bundle exec rails server" yarn start 33 | `) 34 | } 35 | 36 | log('') 37 | log(`Done! Try fetching the Genesis kitty: curl ${process.env.URL_CRYPTO_KITTIES_API}/kitties/1`) 38 | } 39 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("./Migrations.sol") 2 | const GeneScience = artifacts.require("./GeneScience.sol") 3 | const KittyCore = artifacts.require("./KittyCore.sol") 4 | const SaleClockAuction = artifacts.require("./SaleClockAuction.sol") 5 | const SiringClockAuction = artifacts.require("./SiringClockAuction.sol") 6 | 7 | module.exports = (deployer, network) => { 8 | deployer.then(async () => { 9 | const migrations = await deployer.deploy(Migrations) 10 | console.log('Migrations contract deployed at', migrations.address) 11 | 12 | const kittyCore = await deployer.deploy(KittyCore) 13 | console.log('KittyCore contract deployed at', kittyCore.address) 14 | 15 | const ownerCut = 375 16 | 17 | const saleClockAuction = await deployer.deploy(SaleClockAuction, kittyCore.address, ownerCut) 18 | console.log('SaleClockAuction contract deployed at', saleClockAuction.address) 19 | 20 | const siringClockAuction = await deployer.deploy(SiringClockAuction, kittyCore.address, ownerCut) 21 | console.log('SiringClockAuction contract deployed at', siringClockAuction.address) 22 | 23 | const geneScience = await deployer.deploy(GeneScience) 24 | console.log('GeneScience contract deployed at', geneScience.address) 25 | 26 | await kittyCore.setSaleAuctionAddress(saleClockAuction.address) 27 | await kittyCore.setSiringAuctionAddress(siringClockAuction.address) 28 | await kittyCore.setGeneScienceAddress(geneScience.address) 29 | await kittyCore.unpause() 30 | 31 | console.log('KittyCore unpaused') 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /api/routes-cheshire.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Router = require('koa-router') 4 | 5 | const Contract = require('../lib/contract.js') 6 | const Kitty = require('../lib/kitty.js') 7 | const User = require('../lib/user.js') 8 | 9 | module.exports = new Router() 10 | .get('/', (ctx) => { 11 | ctx.body = fs.readFileSync(path.join(__dirname, './static/dashboard.html')).toString() 12 | }) 13 | .get('/cheshire/contracts', (ctx) => { 14 | ctx.body = Contract.addresses 15 | }) 16 | .post('/cheshire/contracts', async (ctx) => { 17 | const { contractName, constructorArgs } = ctx.request.body 18 | const contract = await Contract.deploy(contractName, ...constructorArgs) 19 | 20 | ctx.body = { 21 | address: contract.address, 22 | } 23 | }) 24 | .post('/cheshire/kitties', async (ctx) => { 25 | const { 26 | matronId, 27 | sireId, 28 | generation, 29 | genes, 30 | owner, 31 | apiObject, 32 | } = ctx.request.body 33 | 34 | const kittyId = await Kitty.createKitty(matronId, sireId, generation, genes, owner, apiObject) 35 | 36 | ctx.body = { 37 | kittyId, 38 | } 39 | }) 40 | .post('/cheshire/kitties/import', async (ctx) => { 41 | const { kittyIdMainnet, ownerTestnet } = ctx.request.body 42 | const kittyId = await Kitty.importKitty(kittyIdMainnet, ownerTestnet) 43 | 44 | ctx.body = { 45 | kittyId, 46 | } 47 | }) 48 | .get('/cheshire/users', async (ctx) => { 49 | const users = (await User.findAll(ctx.query.owner_wallet_address)) 50 | .map(row => JSON.parse(row.api_object)) 51 | 52 | ctx.body = { 53 | users, 54 | } 55 | }) 56 | .post('/cheshire/users', async (ctx) => { 57 | const { addressMainnet, addressTestnet } = ctx.request.body 58 | 59 | await User.importUser(addressMainnet, addressTestnet) 60 | 61 | ctx.body = { 62 | address: addressTestnet, 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /test/KittyCoreTest.js: -------------------------------------------------------------------------------- 1 | /* global artifacts */ 2 | /* global contract */ 3 | /* global assert */ 4 | 5 | const GeneScience = artifacts.require('GeneScience') 6 | const KittyCore = artifacts.require('KittyCore') 7 | const SaleClockAuction = artifacts.require('SaleClockAuction') 8 | const SiringClockAuction = artifacts.require('SiringClockAuction') 9 | 10 | contract('KittyCore', async (accounts) => { 11 | const ownerCut = 375 12 | let kittyCore 13 | let saleClockAuction 14 | let siringClockAuction 15 | let geneScience 16 | 17 | // Re-deploy and re-initialize contracts between each test 18 | beforeEach(async () => { 19 | kittyCore = await KittyCore.new() 20 | saleClockAuction = await SaleClockAuction.new(kittyCore.address, ownerCut) 21 | siringClockAuction = await SiringClockAuction.new(kittyCore.address, ownerCut) 22 | geneScience = await GeneScience.new() 23 | 24 | kittyCore.setSaleAuctionAddress(saleClockAuction.address) 25 | kittyCore.setSiringAuctionAddress(siringClockAuction.address) 26 | kittyCore.setGeneScienceAddress(geneScience.address) 27 | kittyCore.unpause() 28 | }) 29 | 30 | describe('approve', async () => { 31 | let kittyId 32 | 33 | beforeEach(async () => { 34 | const kittyArgs = [ 35 | 0, // matron ID 36 | 0, // sire ID 37 | 0, // generation 38 | 0, // genes 39 | accounts[0], // owner 40 | ] 41 | 42 | kittyId = await kittyCore.createKitty.call(...kittyArgs) 43 | kittyCore.createKitty(...kittyArgs) 44 | }) 45 | 46 | it('no account has approval in base state', async () => { 47 | const approvedAddress = await kittyCore.kittyIndexToApproved(kittyId) 48 | assert.equal(0, approvedAddress) 49 | }) 50 | 51 | it('approve should grant approval', async () => { 52 | await kittyCore.approve(accounts[1], kittyId) 53 | const approvedAddress = await kittyCore.kittyIndexToApproved(kittyId) 54 | assert.equal(accounts[1], approvedAddress) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "accounts": [ 3 | { "address": "0x182fc09c33fdd6c2f5b2562f3ca721fa954689c8", "secretKey": "0x76a67ae288fd67ea8d4f7fb94f50c36b606d9448db579584af90d52105f9d8cf", "balance": 1000000000000000000000 }, 4 | { "address": "0x2598fcd1ac410634ca8eb3650fa4c376a529d992", "secretKey": "0x7bc56a099a78d79deba210208ecf214c17e4a913b417dbf854b57edd317eb92f", "balance": 1000000000000000000000 }, 5 | { "address": "0x0644f548e875d7230f63b410b0b82b20c2c171ad", "secretKey": "0x130bac243499b440aeeaba88e14a6dd1e2d17c82a85ba928aeed354699fbd795", "balance": 1000000000000000000000 }, 6 | { "address": "0x1b19148feefe61ec51615c9383884bd47756b257", "secretKey": "0xa9d33ee7ff5359209e6b27a0625a242767ba18218f598ebc2f15733420ad6798", "balance": 1000000000000000000000 }, 7 | { "address": "0x54ae89d6e3398ae0d05e85ae0c854aaac8e951bc", "secretKey": "0x0881c14ffcf978b43eedf57a49d3bcecbbbbb317f6b1098f2d5996b7702744b3", "balance": 1000000000000000000000 }, 8 | { "address": "0x38013fc28df45ec3dc9ca4fc5e95ae488a30a877", "secretKey": "0xca74aa1b79d87563e38d5ff0384ad678c7b176bcd7caa2c503bcfd0004e6567d", "balance": 1000000000000000000000 }, 9 | { "address": "0x847ec3909f604f9c20a17e0ada616d8035375049", "secretKey": "0x99995bdd84af8d1cb2548808d3f2c4ec8dc1b79577856c064f2bf346aeaad2cd", "balance": 1000000000000000000000 }, 10 | { "address": "0x700a66fc44412febcc4e2835ad16eefb054fe09f", "secretKey": "0xe17ad3108883ae14866f3598e65d4d27726efb990f381254d1b909ea783502e4", "balance": 1000000000000000000000 }, 11 | { "address": "0x78788b7ad356053068619f399497712d423eb1e9", "secretKey": "0x44d6627a1e7dcb0d0c353a067ad901449d756c97b38b4feb99c0e752fbbb80d1", "balance": 1000000000000000000000 }, 12 | { "address": "0xcdf40e926a778d93429b72c341b4a9e0ee8624c4", "secretKey": "0x6e77cfded732de6d423abcaccc45ee8c4bdc2eb3c0c47938acb386ac17c496b8", "balance": 1000000000000000000000 } 13 | ], 14 | "ethNodeMainnet": "https://mainnet.infura.io/zLnbh6raC9FoqZqNzfAo", 15 | "addressKittyCoreMainnet": "0x06012c8cf97bead5deae237070f9587f8e7a266d", 16 | "portTestnet": 8546, 17 | "portApi": 4000 18 | } 19 | -------------------------------------------------------------------------------- /lib/contract.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const contract = require('truffle-contract') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const Web3 = require('web3') 6 | 7 | const config = require('../config.json') 8 | 9 | class Contract { 10 | static get addresses() { 11 | const addresses = {} 12 | 13 | const filenames = fs.readdirSync(path.join(__dirname, '../contracts')) 14 | filenames.forEach((filename) => { 15 | if (!filename.match(/\.sol$/)) { 16 | return 17 | } 18 | 19 | const contractName = filename.replace(/\.sol$/, '') 20 | addresses[contractName] = process.env[`ADDRESS_${_.snakeCase(contractName).toUpperCase()}`] 21 | }) 22 | 23 | return addresses 24 | } 25 | 26 | static get contractDefaults() { 27 | return { 28 | from: config.accounts[0].address, 29 | gas: 6500000, 30 | gasPrice: 100000000000, 31 | } 32 | } 33 | 34 | static declaration(contractName) { 35 | // eslint-disable-next-line global-require, import/no-dynamic-require 36 | const declaration = contract(require(`../build/contracts/${contractName}.json`)) 37 | declaration.defaults(this.contractDefaults) 38 | declaration.setProvider(this.web3Provider) 39 | 40 | return declaration 41 | } 42 | 43 | static declarationMainnet(contractName) { 44 | const declaration = Contract.declaration(contractName) 45 | declaration.setProvider(this.web3ProviderMainnet) 46 | 47 | return declaration 48 | } 49 | 50 | static async deploy(contractName, ...constructorArgs) { 51 | const newContract = constructorArgs.length > 0 52 | ? await this.declaration(contractName).new(...constructorArgs) 53 | : await this.declaration(contractName).new() 54 | 55 | process.env[`ADDRESS_${_.snakeCase(contractName).toUpperCase()}`] = newContract.address 56 | 57 | return newContract 58 | } 59 | 60 | static get web3Provider() { 61 | return new Web3.providers.HttpProvider(`http://localhost:${config.portTestnet}`) 62 | } 63 | 64 | static get web3ProviderMainnet() { 65 | return new Web3.providers.HttpProvider(config.ethNodeMainnet) 66 | } 67 | } 68 | 69 | module.exports = Contract 70 | -------------------------------------------------------------------------------- /lib/user.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const Kitty = require('./kitty.js') 3 | 4 | class User { 5 | static async count() { 6 | return new Promise((resolve, reject) => { 7 | this.db.get('SELECT COUNT(*) FROM users', (err, row) => { 8 | if (err) { 9 | reject(err) 10 | } else { 11 | resolve(row['COUNT(*)']) 12 | } 13 | }) 14 | }) 15 | } 16 | 17 | static createUser(addressMainnet, addressTestnet, apiObject) { 18 | return new Promise((resolve, reject) => { 19 | this.db.run('INSERT INTO users (address_mainnet, address_testnet, api_object) VALUES (?, ?, ?)', addressMainnet, addressTestnet, JSON.stringify(apiObject), (err) => { 20 | if (err) { 21 | reject(err) 22 | } else { 23 | resolve(addressTestnet) 24 | } 25 | }) 26 | }) 27 | } 28 | 29 | static async fetchAttrsApi(address) { 30 | return (await axios.get(`https://api.cryptokitties.co/user/${address}`)).data 31 | } 32 | 33 | static async findAll() { 34 | return new Promise((resolve, reject) => { 35 | this.db.all('SELECT * FROM users', (err, rows) => { 36 | if (err) { 37 | reject(err) 38 | } else { 39 | resolve(rows) 40 | } 41 | }) 42 | }) 43 | } 44 | 45 | static async findByAddress(address) { 46 | return new Promise((resolve, reject) => { 47 | this.db.get('SELECT * FROM users WHERE address_testnet=?', address, (err, row) => { 48 | if (err) { 49 | reject(err) 50 | } else { 51 | resolve(row) 52 | } 53 | }) 54 | }) 55 | } 56 | 57 | static async importUser(addressMainnet, addressTestnet) { 58 | const attrsApi = await this.fetchAttrsApi(addressMainnet) 59 | attrsApi.address = addressTestnet 60 | 61 | this.db.run('UPDATE users SET address_mainnet=?, api_object=? WHERE address_testnet=?', addressMainnet, JSON.stringify(attrsApi), addressTestnet) 62 | 63 | const { kitties } = (await axios.get(`https://api.cryptokitties.co/kitties?owner_wallet_address=${addressMainnet}`)).data 64 | 65 | // Need to import kitties in serial because the kitty ID "predicted" by 66 | // `createKitty.call()` lags so far behind the actual `createKitty` 67 | // transaction that we almost always get duplicate/inaccurate kitty IDs 68 | // in the database. 69 | // 70 | // Potential solution: drive kitty INSERTS with the `Birth` event. 71 | // 72 | // eslint-disable-next-line no-restricted-syntax 73 | for (const kitty of kitties) { 74 | await Kitty.importKitty(kitty.id, addressTestnet) // eslint-disable-line no-await-in-loop 75 | } 76 | 77 | return addressTestnet 78 | } 79 | } 80 | 81 | User.db = require('../api/db.js') 82 | 83 | module.exports = User 84 | -------------------------------------------------------------------------------- /lib/cheshire.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const axios = require('axios') 3 | 4 | const Contract = require('./contract.js') 5 | 6 | class Cheshire { 7 | constructor(config) { 8 | this.config = config 9 | 10 | process.env.URL_CRYPTO_KITTIES_API = `http://localhost:${config.portApi}` 11 | } 12 | 13 | get accounts() { 14 | return this.config.accounts 15 | } 16 | 17 | contractAddress(contractName) { // eslint-disable-line class-methods-use-this 18 | return process.env[`ADDRESS_${_.snakeCase(contractName).toUpperCase()}`] 19 | } 20 | 21 | contractInstance(contractName) { 22 | return Contract.declaration(contractName).at(this.contractAddress(contractName)) 23 | } 24 | 25 | async createKitty(matronId, sireId, generation, genes, owner, apiObject) { 26 | const params = { 27 | matronId, 28 | sireId, 29 | generation, 30 | genes, 31 | owner, 32 | apiObject, 33 | } 34 | 35 | return (await axios.post(`http://localhost:${this.config.portApi}/cheshire/kitties`, params)).data.kittyId 36 | } 37 | 38 | async deployContract(contractName, ...constructorArgs) { 39 | const params = { 40 | contractName, 41 | constructorArgs, 42 | } 43 | 44 | await axios.post(`http://localhost:${this.config.portApi}/cheshire/contracts`, params) 45 | 46 | return this.contractInstance(contractName) 47 | } 48 | 49 | async importKitty(kittyIdMainnet, ownerTestnet) { 50 | const params = { 51 | kittyIdMainnet, 52 | ownerTestnet, 53 | } 54 | 55 | return (await axios.post(`http://localhost:${this.config.portApi}/cheshire/kitties/import`, params)).data.kittyId 56 | } 57 | 58 | async importUser(addressMainnet, addressTestnet) { 59 | const params = { 60 | addressMainnet, 61 | addressTestnet, 62 | } 63 | 64 | return (await axios.post(`http://localhost:${this.config.portApi}/cheshire/users`, params)).data.address 65 | } 66 | 67 | async printHelp() { 68 | const { log } = console 69 | 70 | log('Available Accounts') 71 | log('====================') 72 | 73 | this.config.accounts.forEach((account, index) => log(`(${index}) ${account.address}`)) 74 | 75 | log('') 76 | 77 | log('Private Keys') 78 | log('====================') 79 | 80 | this.config.accounts.forEach((account, index) => log(`(${index}) ${account.secretKey}`)) 81 | 82 | log('') 83 | 84 | log('Testnet Contracts') 85 | log('====================') 86 | const contracts = Contract.addresses 87 | _.forOwn(contracts, (contractAddress, contractName) => { 88 | log(`${contractName}: ${contractAddress}`) 89 | }) 90 | 91 | log('') 92 | 93 | log('Services') 94 | log('====================') 95 | log(`Ethereum testnet listening on port ${this.config.portTestnet}`) 96 | log(`CryptoKitties API listening on port ${this.config.portApi}`) 97 | log(`Cheshire dashboard available at http://localhost:${this.config.portApi}`) 98 | 99 | log('') 100 | log('View the above at any time by running `yarn run help`') 101 | log('') 102 | } 103 | } 104 | 105 | module.exports = Cheshire 106 | -------------------------------------------------------------------------------- /test-cheshire/contract.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const Contract = require('../lib/contract.js') 4 | const config = require('../config.json') 5 | 6 | describe('contract', () => { 7 | it('addresses getter should return contract addresses from environment', () => { 8 | jest 9 | .spyOn(fs, 'readdirSync') 10 | .mockReturnValue(['ContractOne.sol', 'TextFile.txt']) 11 | 12 | process.env.ADDRESS_CONTRACT_ONE = '0x111' 13 | 14 | expect(Object.keys(Contract.addresses).length).toBe(1) 15 | expect(Contract.addresses.ContractOne).toBe('0x111') 16 | }) 17 | 18 | it('contractDefaults should return reasonable defaults', () => { 19 | expect(Contract.contractDefaults).toEqual({ 20 | from: config.accounts[0].address, 21 | gas: 6500000, 22 | gasPrice: 100000000000, 23 | }) 24 | }) 25 | 26 | it('declaration should return testnet contract declaration', () => { 27 | const declaration = Contract.declaration('KittyCore') 28 | 29 | expect(declaration.contractName).toBe('KittyCore') 30 | expect(declaration.currentProvider.host).toBe(`http://localhost:${config.portTestnet}`) 31 | expect(declaration.class_defaults).toEqual({ 32 | from: config.accounts[0].address, 33 | gas: 6500000, 34 | gasPrice: 100000000000, 35 | }) 36 | }) 37 | 38 | it('declarationMainnet should return mainnet contract declaration', () => { 39 | const declaration = Contract.declarationMainnet('KittyCore') 40 | 41 | expect(declaration.contractName).toBe('KittyCore') 42 | expect(declaration.currentProvider.host).toBe(config.ethNodeMainnet) 43 | }) 44 | 45 | it('deploy should deploy contracts to testnet', async () => { 46 | const mockNew = jest.fn().mockResolvedValue({ address: '0x112233' }) 47 | Contract.declaration = jest.fn() 48 | .mockReturnValue({ 49 | new: mockNew, 50 | }) 51 | 52 | const instance = await Contract.deploy('TestContract') 53 | 54 | expect(instance.address).toBe('0x112233') 55 | expect(process.env.ADDRESS_TEST_CONTRACT).toBe('0x112233') 56 | expect(mockNew).toHaveBeenCalledTimes(1) 57 | expect(Contract.declaration).toHaveBeenCalledWith('TestContract') 58 | }) 59 | 60 | it('deploy should deploy contracts having constructor arguments to testnet', async () => { 61 | const mockNew = jest.fn().mockResolvedValue({ address: '0x223344' }) 62 | Contract.declaration = jest.fn() 63 | .mockReturnValue({ 64 | new: mockNew, 65 | }) 66 | 67 | const instance = await Contract.deploy('TestContract', 'arg 1') 68 | 69 | expect(instance.address).toBe('0x223344') 70 | expect(process.env.ADDRESS_TEST_CONTRACT).toBe('0x223344') 71 | expect(Contract.declaration).toHaveBeenCalledTimes(1) 72 | expect(Contract.declaration).toHaveBeenCalledWith('TestContract') 73 | expect(mockNew).toHaveBeenCalledTimes(1) 74 | expect(mockNew).toHaveBeenCalledWith('arg 1') 75 | }) 76 | 77 | it('web3Provider should return testnet provider', () => { 78 | const provider = Contract.web3Provider 79 | expect(provider.host).toBe(`http://localhost:${config.portTestnet}`) 80 | }) 81 | 82 | it('web3ProviderMainnet should return mainnet provider', () => { 83 | const provider = Contract.web3ProviderMainnet 84 | expect(provider.host).toBe(config.ethNodeMainnet) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /api/static/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cheshire Dashboard 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 |
24 |
25 |
26 |
27 |

Contracts

28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
NameAddress
{{ name }}{{ address }}
42 |
43 | No contracts found 44 |
45 | 46 |

Kitties

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 65 | 66 | 74 | 75 | 76 |
ID (testnet)NameOwnerAttributes
{{ kitty.id }} 62 | {{ kitty.name }} 63 | No name 64 | {{ kitty.owner.address }} 67 | 68 | Show 69 | Hide 70 | 71 | 72 |
{{ kitty }}
73 |
77 |
78 | No kitties found 79 |
80 | 81 |

Users

82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
AddressNicknameAttributes
{{ user.address }} {{ user.nickname }}
{{ user }}
98 |
99 | No users found 100 |
101 |
102 |
103 |
104 |
105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const ganache = require('ganache-cli') 3 | const path = require('path') 4 | const { exec } = require('child_process') 5 | 6 | const Api = require('./api') 7 | const Cheshire = require('./lib/cheshire.js') 8 | const config = require('./config.json') 9 | const Contract = require('./lib/contract.js') 10 | const User = require('./lib/user.js') 11 | 12 | const { log } = console 13 | 14 | const Server = { 15 | startTestnet() { 16 | log('> Starting testnet...') 17 | 18 | ganache.server({ 19 | accounts: config.accounts, 20 | // debug: true, 21 | // logger: console, 22 | // verbose: true, 23 | }) 24 | .listen(config.portTestnet, (err) => { 25 | if (err) { 26 | log('Error starting Ganache:', err) 27 | process.exit(1) 28 | } 29 | }) 30 | }, 31 | 32 | async compileContracts() { 33 | log('> Compiling contracts...') 34 | 35 | return new Promise((resolve, reject) => { 36 | const child = exec('truffle compile') 37 | child.stdout.pipe(process.stdout) 38 | child.stderr.pipe(process.stderr) 39 | child.on('error', log) 40 | child.on('close', (code) => { 41 | if (code === 0) { 42 | resolve() 43 | } else { 44 | log('Exited with code', code) 45 | reject() 46 | } 47 | }) 48 | }) 49 | }, 50 | 51 | async deployContracts() { 52 | log('> Deploying CryptoKitties contracts to testnet...') 53 | 54 | const ownerCut = 375 55 | 56 | const kittyCore = await Contract.deploy('KittyCore') 57 | const saleClockAuction = await Contract.deploy('SaleClockAuction', kittyCore.address, ownerCut) 58 | const siringClockAuction = await Contract.deploy('SiringClockAuction', kittyCore.address, ownerCut) 59 | const geneScience = await Contract.deploy('GeneScience') 60 | 61 | await kittyCore.setSaleAuctionAddress(saleClockAuction.address) 62 | await kittyCore.setSiringAuctionAddress(siringClockAuction.address) 63 | await kittyCore.setGeneScienceAddress(geneScience.address) 64 | await kittyCore.unpause() 65 | 66 | return { 67 | kittyCore: kittyCore.address, 68 | saleClockAuction: saleClockAuction.address, 69 | siringClockAuction: siringClockAuction.address, 70 | geneScience: geneScience.address, 71 | } 72 | }, 73 | 74 | async startApiServer() { 75 | log('> Starting local CryptoKitties API server...') 76 | 77 | Api.listen(config.portApi) 78 | }, 79 | 80 | async loadAccounts() { 81 | log('> Loading accounts') 82 | 83 | config.accounts.forEach(async (account, index) => { 84 | const apiObject = { 85 | address: account.address, 86 | nickname: `User #${index}`, 87 | image: '1', 88 | } 89 | 90 | await User.createUser(account.address, account.address, apiObject) 91 | }) 92 | }, 93 | 94 | async runSetupScript() { 95 | log('> Running setup script...') 96 | 97 | const scriptPath = path.join(process.cwd(), process.argv[2] || './scripts/setup.js') 98 | if (!fs.existsSync(scriptPath)) { 99 | log('Exiting, script not found:', scriptPath) 100 | process.exit(1) 101 | } 102 | 103 | const cheshire = new Cheshire(config) 104 | 105 | console.group() 106 | // eslint-disable-next-line global-require, import/no-dynamic-require 107 | await require(scriptPath)(cheshire) 108 | console.groupEnd() 109 | }, 110 | 111 | async start() { 112 | await this.startTestnet() 113 | await this.compileContracts() 114 | await this.deployContracts() 115 | await this.startApiServer() 116 | await this.loadAccounts() 117 | await this.runSetupScript() 118 | 119 | log('') 120 | log('Cheshire is live 😺 Here\'s what\'s inside:') 121 | log('') 122 | 123 | await new Cheshire(config).printHelp() 124 | }, 125 | } 126 | 127 | Server.start() 128 | -------------------------------------------------------------------------------- /lib/kitty.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const config = require('../config.json') 4 | const Contract = require('./contract.js') 5 | 6 | class Kitty { 7 | static count(ownerWalletAddress) { 8 | return new Promise((resolve, reject) => { 9 | // TODO: use query builder 10 | const sql = ownerWalletAddress 11 | ? ['SELECT COUNT(*) FROM kitties WHERE LOWER(owner)=LOWER(?)', ownerWalletAddress] 12 | : ['SELECT COUNT(*) FROM kitties'] 13 | 14 | this.db.get(...sql, (err, row) => { 15 | if (err) { 16 | reject(err) 17 | } else { 18 | resolve(row['COUNT(*)']) 19 | } 20 | }) 21 | }) 22 | } 23 | 24 | static async createKitty(matronId, sireId, generation, genes, owner, apiObject) { 25 | const kittyCore = Contract.declaration('KittyCore').at(process.env.ADDRESS_KITTY_CORE) 26 | 27 | const kittyId = parseInt(await kittyCore.createKitty.call( 28 | matronId, 29 | sireId, 30 | generation, 31 | genes, 32 | owner, 33 | { value: 0, gas: 500000, gasPrice: 10000000000 } // eslint-disable-line comma-dangle 34 | ), 10) 35 | 36 | await kittyCore.createKitty( 37 | matronId, 38 | sireId, 39 | generation, 40 | genes, 41 | owner, 42 | { value: 0, gas: 500000, gasPrice: 10000000000 } // eslint-disable-line comma-dangle 43 | ) 44 | 45 | const apiObjectTestnet = apiObject 46 | apiObjectTestnet.id = kittyId 47 | if (!apiObjectTestnet.owner) { 48 | apiObjectTestnet.owner = {} 49 | } 50 | apiObjectTestnet.owner.address = owner 51 | 52 | return new Promise((resolve, reject) => { 53 | this.db.run('INSERT INTO kitties (id_testnet, owner, api_object) VALUES (?, ?, ?)', kittyId, owner, JSON.stringify(apiObjectTestnet), (err) => { 54 | if (err) { 55 | reject(err) 56 | } else { 57 | resolve(kittyId) 58 | } 59 | }) 60 | }) 61 | } 62 | 63 | static async fetchAttrsApi(kittyId) { 64 | return (await axios.get(`https://api.cryptokitties.co/kitties/${kittyId}`)).data 65 | } 66 | 67 | static async fetchAttrsChain(kittyId) { 68 | const kittyCoreMainnet = Contract.declarationMainnet('KittyCore').at(config.addressKittyCoreMainnet) 69 | const [ 70 | isGestating, 71 | isReady, 72 | cooldownIndex, 73 | nextActionAt, 74 | siringWithId, 75 | birthTime, 76 | matronId, 77 | sireId, 78 | generation, 79 | genes, 80 | ] = await kittyCoreMainnet.getKitty(kittyId) 81 | 82 | return { 83 | isGestating, 84 | isReady, 85 | cooldownIndex, 86 | nextActionAt, 87 | siringWithId, 88 | birthTime, 89 | matronId, 90 | sireId, 91 | generation, 92 | genes, 93 | } 94 | } 95 | 96 | static findAll(ownerWalletAddress, limit, offset) { 97 | return new Promise((resolve, reject) => { 98 | // TODO: use query builder 99 | const sql = ownerWalletAddress 100 | ? ['SELECT api_object FROM kitties WHERE LOWER(owner) = LOWER(?) LIMIT ? OFFSET ?', ownerWalletAddress, limit, offset] 101 | : ['SELECT api_object FROM kitties LIMIT ? OFFSET ?', limit, offset] 102 | 103 | this.db.all(...sql, (err, rows) => { 104 | if (err) { 105 | reject(err) 106 | } else { 107 | resolve(rows) 108 | } 109 | }) 110 | }) 111 | } 112 | 113 | static findById(id) { 114 | return new Promise((resolve, reject) => { 115 | this.db.get('SELECT * FROM kitties WHERE id_testnet=?', id, (err, row) => { 116 | if (err) { 117 | reject(err) 118 | } else { 119 | resolve(row) 120 | } 121 | }) 122 | }) 123 | } 124 | 125 | static async importKitty(kittyIdMainnet, ownerTestnet) { 126 | const attrsChain = await this.fetchAttrsChain(kittyIdMainnet) 127 | const attrsApi = await this.fetchAttrsApi(kittyIdMainnet) 128 | 129 | const kittyId = await this.createKitty( 130 | attrsChain.matronId, 131 | attrsChain.sireId, 132 | attrsChain.generation, 133 | attrsChain.genes, 134 | ownerTestnet, 135 | attrsApi, 136 | ) 137 | 138 | return kittyId 139 | } 140 | } 141 | 142 | Kitty.db = require('../api/db.js') 143 | 144 | module.exports = Kitty 145 | -------------------------------------------------------------------------------- /test-cheshire/routes-cryptokitties.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | 3 | const Api = require('../api') 4 | const Kitty = require('../lib/kitty.js') 5 | const User = require('../lib/user.js') 6 | 7 | describe('routes-cryptokitties', () => { 8 | const api = Api.listen(0) 9 | 10 | afterAll(() => { 11 | api.close() 12 | }) 13 | 14 | it('GET /kitties/:id should return kitty API object', async () => { 15 | Kitty.findById = jest.fn().mockResolvedValue({ api_object: '{"name": "cheshire"}' }) 16 | 17 | await request(api) 18 | .get('/kitties/234') 19 | .expect('Content-Type', /json/) 20 | .expect(200) 21 | .then((response) => { 22 | expect(JSON.parse(response.text)).toEqual({ 23 | name: 'cheshire', 24 | }) 25 | 26 | expect(Kitty.findById).toHaveBeenCalledTimes(1) 27 | expect(Kitty.findById).toHaveBeenCalledWith(234) 28 | }) 29 | }) 30 | 31 | it('GET /kitties/:id should return 404 status code for missing kitties', async () => { 32 | Kitty.findById = jest.fn().mockResolvedValue(undefined) 33 | 34 | await request(api) 35 | .get('/kitties/234') 36 | .expect('Content-Type', /plain/) 37 | .expect(404) 38 | .then((response) => { 39 | expect(response.text).toBe('Not Found') 40 | 41 | expect(Kitty.findById).toHaveBeenCalledTimes(1) 42 | expect(Kitty.findById).toHaveBeenCalledWith(234) 43 | }) 44 | }) 45 | 46 | it('GET /kitties should return kitties associated with owner_wallet_address', async () => { 47 | Kitty.findAll = jest.fn().mockResolvedValue([ 48 | { api_object: '{"name": "cheshire"}' }, 49 | { api_object: '{"name": "snowball"}' }, 50 | ]) 51 | Kitty.count = jest.fn().mockReturnValue(2) 52 | 53 | await request(api) 54 | .get('/kitties?owner_wallet_address=0x123&limit=5&offset=0') 55 | .expect('Content-Type', /json/) 56 | .expect(200) 57 | .then((response) => { 58 | expect(JSON.parse(response.text)).toEqual({ 59 | kitties: [ 60 | { name: 'cheshire' }, 61 | { name: 'snowball' }, 62 | ], 63 | limit: 5, 64 | offset: 0, 65 | total: 2, 66 | }) 67 | 68 | expect(Kitty.findAll).toHaveBeenCalledTimes(1) 69 | expect(Kitty.findAll).toHaveBeenCalledWith('0x123', 5, 0) 70 | expect(Kitty.count).toHaveBeenCalledTimes(1) 71 | }) 72 | }) 73 | 74 | it('GET /kitties should use default limit and offset', async () => { 75 | Kitty.findAll = jest.fn().mockResolvedValue([ 76 | { api_object: '{"name": "cheshire"}' }, 77 | { api_object: '{"name": "snowball"}' }, 78 | ]) 79 | Kitty.count = jest.fn().mockReturnValue(2) 80 | 81 | await request(api) 82 | .get('/kitties?owner_wallet_address=0x123') 83 | .expect('Content-Type', /json/) 84 | .expect(200) 85 | .then((response) => { 86 | expect(JSON.parse(response.text)).toEqual({ 87 | kitties: [ 88 | { name: 'cheshire' }, 89 | { name: 'snowball' }, 90 | ], 91 | limit: 12, 92 | offset: 0, 93 | total: 2, 94 | }) 95 | 96 | expect(Kitty.findAll).toHaveBeenCalledTimes(1) 97 | expect(Kitty.findAll).toHaveBeenCalledWith('0x123', 12, 0) 98 | expect(Kitty.count).toHaveBeenCalledTimes(1) 99 | }) 100 | }) 101 | 102 | it('GET /user/:address should return user API object', async () => { 103 | User.findByAddress = jest.fn().mockResolvedValue({ 104 | address: '0x123ABC', 105 | api_object: '{"address": "0x123abc", "nickname":"Eugene", "image": "19"}', 106 | }) 107 | 108 | await request(api) 109 | .get('/user/0x123ABC') 110 | .expect('Content-Type', /json/) 111 | .expect(200) 112 | .then((response) => { 113 | expect(JSON.parse(response.text)).toEqual({ 114 | address: '0x123abc', 115 | nickname: 'Eugene', 116 | image: '19', 117 | }) 118 | 119 | expect(User.findByAddress).toHaveBeenCalledTimes(1) 120 | expect(User.findByAddress).toHaveBeenCalledWith('0x123ABC') 121 | }) 122 | }) 123 | 124 | it('GET /user/:id should return 404 status code for missing users', async () => { 125 | User.findByAddress = jest.fn().mockResolvedValue(undefined) 126 | 127 | await request(api) 128 | .get('/user/0x234') 129 | .expect('Content-Type', /json/) 130 | .expect(404) 131 | .then((response) => { 132 | expect(JSON.parse(response.text)).toEqual({ 133 | error: 'user not found', 134 | }) 135 | 136 | expect(User.findByAddress).toHaveBeenCalledTimes(1) 137 | expect(User.findByAddress).toHaveBeenCalledWith('0x234') 138 | }) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /test-cheshire/routes-cheshire.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | 3 | const Api = require('../api') 4 | const Contract = require('../lib/contract.js') 5 | const Kitty = require('../lib/kitty.js') 6 | const User = require('../lib/user.js') 7 | 8 | describe('routes-cheshire', () => { 9 | const api = Api.listen(0) 10 | 11 | afterAll(() => { 12 | api.close() 13 | }) 14 | 15 | it('GET / should serve dashboard', async () => { 16 | await request(api) 17 | .get('/') 18 | .expect('Content-Type', /text/) 19 | .expect(200) 20 | .then((response) => { 21 | expect(response.text).toContain('Cheshire Dashboard') 22 | }) 23 | }) 24 | 25 | it('GET /cheshire/contracts should return contract addresses', async () => { 26 | process.env.ADDRESS_KITTY_CORE = '0x111' 27 | process.env.ADDRESS_SALE_CLOCK_AUCTION = '0x222' 28 | process.env.ADDRESS_SIRING_CLOCK_AUCTION = '0x333' 29 | 30 | await request(api) 31 | .get('/cheshire/contracts') 32 | .expect('Content-Type', /json/) 33 | .expect(200) 34 | .then((response) => { 35 | expect(JSON.parse(response.text)).toEqual({ 36 | KittyCore: '0x111', 37 | SaleClockAuction: '0x222', 38 | SiringClockAuction: '0x333', 39 | }) 40 | }) 41 | }) 42 | 43 | it('POST /cheshire/contracts should deploy contract', async () => { 44 | Contract.deploy = jest.fn().mockReturnValue({ address: '0x111' }) 45 | 46 | await request(api) 47 | .post('/cheshire/contracts') 48 | .send({ contractName: 'KittyRace', constructorArgs: [] }) 49 | .set('Accept', 'application/json') 50 | .expect(200) 51 | .then((response) => { 52 | expect(JSON.parse(response.text)).toEqual({ 53 | address: '0x111', 54 | }) 55 | 56 | expect(Contract.deploy).toHaveBeenCalledTimes(1) 57 | expect(Contract.deploy).toHaveBeenCalledWith('KittyRace') 58 | }) 59 | }) 60 | 61 | it('POST /cheshire/kitties should create kitty', async () => { 62 | Kitty.createKitty = jest.fn().mockReturnValue(23) 63 | 64 | await request(api) 65 | .post('/cheshire/kitties') 66 | .send({ 67 | matronId: 1, 68 | sireId: 2, 69 | generation: 3, 70 | genes: 4, 71 | owner: '0x123', 72 | apiObject: { name: 'cheshire' }, 73 | }) 74 | .set('Accept', 'application/json') 75 | .expect(200) 76 | .then((response) => { 77 | expect(JSON.parse(response.text)).toEqual({ 78 | kittyId: 23, 79 | }) 80 | 81 | expect(Kitty.createKitty).toHaveBeenCalledTimes(1) 82 | expect(Kitty.createKitty).toHaveBeenCalledWith( 83 | 1, // matron ID 84 | 2, // sire Id 85 | 3, // generation 86 | 4, // genes 87 | '0x123', // owner 88 | { name: 'cheshire' }, // apiObject 89 | ) 90 | }) 91 | }) 92 | 93 | it('POST /cheshire/kitties/import should import kitty', async () => { 94 | Kitty.importKitty = jest.fn().mockReturnValue(111) 95 | 96 | await request(api) 97 | .post('/cheshire/kitties/import') 98 | .send({ kittyIdMainnet: 1, ownerTestnet: '0x222' }) 99 | .set('Accept', 'application/json') 100 | .expect(200) 101 | .then((response) => { 102 | expect(JSON.parse(response.text)).toEqual({ 103 | kittyId: 111, 104 | }) 105 | 106 | expect(Kitty.importKitty).toHaveBeenCalledTimes(1) 107 | expect(Kitty.importKitty).toHaveBeenCalledWith(1, '0x222') 108 | }) 109 | }) 110 | 111 | it('POST /cheshire/users should import user', async () => { 112 | User.importUser = jest.fn().mockReturnValue('0x111abc') 113 | 114 | await request(api) 115 | .post('/cheshire/users') 116 | .send({ addressMainnet: '0x111ABC', addressTestnet: '0x222DEF' }) 117 | .set('Accept', 'application/json') 118 | .expect(200) 119 | .then((response) => { 120 | expect(JSON.parse(response.text)).toEqual({ 121 | address: '0x222DEF', 122 | }) 123 | 124 | expect(User.importUser).toHaveBeenCalledTimes(1) 125 | expect(User.importUser).toHaveBeenCalledWith('0x111ABC', '0x222DEF') 126 | }) 127 | }) 128 | 129 | it('GET /cheshire/users should return users list', async () => { 130 | User.findAll = jest.fn().mockReturnValue([{ api_object: '{ "name": "eugene" }' }]) 131 | 132 | await request(api) 133 | .get('/cheshire/users') 134 | .set('Accept', 'application/json') 135 | .expect(200) 136 | .then((response) => { 137 | expect(JSON.parse(response.text)).toEqual({ 138 | users: [ 139 | { name: 'eugene' }, 140 | ], 141 | }) 142 | 143 | expect(User.findAll).toHaveBeenCalledTimes(1) 144 | }) 145 | }) 146 | }) 147 | -------------------------------------------------------------------------------- /test-cheshire/cheshire.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const Cheshire = require('../lib/cheshire.js') 4 | const config = require('../config.json') 5 | const Contract = require('../lib/contract.js') 6 | 7 | const cheshire = new Cheshire(config) 8 | 9 | describe('cheshire', () => { 10 | it('constructor should set URL_CRYPTO_KITTIES_API env var', () => { 11 | expect(process.env.URL_CRYPTO_KITTIES_API).toBe('http://localhost:4000') 12 | }) 13 | 14 | it('accounts getter should return accounts defined in config', () => { 15 | expect(cheshire.accounts).toEqual(config.accounts) 16 | expect(cheshire.accounts.length).toBe(10) 17 | expect(cheshire.accounts[0].address).toBe('0x182fc09c33fdd6c2f5b2562f3ca721fa954689c8') 18 | }) 19 | 20 | it('contractAddress should return given contract address', () => { 21 | process.env.ADDRESS_TEST = '0x123' 22 | 23 | expect(cheshire.contractAddress('test')).toBe('0x123') 24 | }) 25 | 26 | it('contractInstance should return instance of given contract', () => { 27 | const mockAt = jest.fn().mockReturnValue({ address: '0x777' }) 28 | Contract.declaration = jest.fn() 29 | .mockReturnValue({ 30 | at: mockAt, 31 | }) 32 | cheshire.contractAddress = jest.fn().mockReturnValue('0x777') 33 | 34 | const instance = cheshire.contractInstance('TestContract') 35 | 36 | expect(instance.address).toBe('0x777') 37 | expect(Contract.declaration).toHaveBeenCalledTimes(1) 38 | expect(Contract.declaration).toHaveBeenCalledWith('TestContract') 39 | expect(mockAt).toHaveBeenCalledTimes(1) 40 | expect(mockAt).toHaveBeenCalledWith('0x777') 41 | }) 42 | 43 | it('deployContract should POST to /cheshire/contracts', async () => { 44 | axios.post = jest.fn() 45 | .mockReturnValue({ data: { address: '0x001122' } }) 46 | 47 | jest 48 | .spyOn(cheshire, 'contractInstance') 49 | .mockReturnValue({ address: '0x001122' }) 50 | 51 | const contractInstance = await cheshire.deployContract('TestContract') 52 | 53 | expect(axios.post).toHaveBeenCalledTimes(1) 54 | expect(axios.post).toHaveBeenCalledWith( 55 | `http://localhost:${config.portApi}/cheshire/contracts`, 56 | { 57 | contractName: 'TestContract', 58 | constructorArgs: [], 59 | }, 60 | ) 61 | expect(contractInstance.address).toBe('0x001122') 62 | }) 63 | 64 | it('createKitty should POST to /cheshire/kitties', async () => { 65 | axios.post = jest.fn().mockReturnValue({ data: { kittyId: 123 } }) 66 | 67 | const kittyId = await cheshire.createKitty( 68 | 1, // matronId 69 | 2, // sireId 70 | 3, // generation 71 | 4, // genes 72 | '0x123', // owner 73 | { name: 'cheshire' }, // apiObject 74 | ) 75 | 76 | expect(axios.post).toHaveBeenCalledTimes(1) 77 | expect(axios.post).toHaveBeenCalledWith( 78 | `http://localhost:${config.portApi}/cheshire/kitties`, 79 | { 80 | matronId: 1, 81 | sireId: 2, 82 | generation: 3, 83 | genes: 4, 84 | owner: '0x123', 85 | apiObject: { name: 'cheshire' }, 86 | }, 87 | ) 88 | expect(kittyId).toBe(123) 89 | }) 90 | 91 | it('importKitty should POST to /cheshire/kitties/import', async () => { 92 | axios.post = jest.fn().mockReturnValue({ data: { kittyId: 1 } }) 93 | 94 | const kittyIdTestnet = await cheshire.importKitty(101, config.accounts[0].address) 95 | 96 | expect(axios.post).toHaveBeenCalledTimes(1) 97 | expect(axios.post).toHaveBeenCalledWith( 98 | `http://localhost:${config.portApi}/cheshire/kitties/import`, 99 | { 100 | kittyIdMainnet: 101, 101 | ownerTestnet: config.accounts[0].address, 102 | }, 103 | ) 104 | expect(kittyIdTestnet).toBe(1) 105 | }) 106 | 107 | it('importUser should POST to /cheshire/users', async () => { 108 | axios.post = jest.fn().mockReturnValue({ data: { address: '0x234' } }) 109 | 110 | const addressTestnet = await cheshire.importUser('0x123', '0x234') 111 | 112 | expect(addressTestnet).toBe('0x234') 113 | expect(axios.post).toHaveBeenCalledTimes(1) 114 | expect(axios.post).toHaveBeenCalledWith( 115 | `http://localhost:${config.portApi}/cheshire/users`, 116 | { 117 | addressMainnet: '0x123', 118 | addressTestnet: '0x234', 119 | }, 120 | ) 121 | }) 122 | 123 | it('prints help', async () => { 124 | global.console.log = jest.fn() 125 | 126 | cheshire.printHelp() 127 | 128 | const helpText = console.log.mock.calls.map(call => call[0]).join('') 129 | 130 | expect(helpText).toMatch(/Available Accounts/) 131 | expect(helpText).toMatch(/\(0\) 0x182fc09c33fdd6c2f5b2562f3ca721fa954689c8/) 132 | 133 | expect(helpText).toMatch(/Private Keys/) 134 | expect(helpText).toMatch(/\(0\) 0x76a67ae288fd67ea8d4f7fb94f50c36b606d9448db579584af90d52105f9d8cf/) 135 | 136 | expect(helpText).toMatch(/Testnet Contracts/) 137 | expect(helpText).toMatch(/KittyCore/) 138 | expect(helpText).toMatch(/SaleClockAuction/) 139 | expect(helpText).toMatch(/SiringClockAuction/) 140 | 141 | expect(helpText).toMatch(/Services/) 142 | expect(helpText).toMatch(/Ethereum testnet.*port 8546/) 143 | expect(helpText).toMatch(/CryptoKitties API.*http:\/\/localhost:4000/) 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /test-cheshire/user.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const User = require('../lib/user.js') 4 | const Kitty = require('../lib/kitty.js') 5 | 6 | describe('user', () => { 7 | beforeAll(() => { 8 | User.db.serialize(() => { 9 | User.db.run('INSERT INTO users (address_mainnet, address_testnet, api_object) VALUES (?, ?, ?)', '0x123', '0x456', JSON.stringify({ name: 'Eugene' })) 10 | User.db.run('INSERT INTO users (address_mainnet, address_testnet, api_object) VALUES (?, ?, ?)', '0x234', '0x345', JSON.stringify({ name: 'Charley' })) 11 | }) 12 | }) 13 | 14 | afterAll(() => { 15 | User.db.close() 16 | }) 17 | 18 | it('count should return total number of users in database', async () => { 19 | const count = await User.count() 20 | expect(count).toBe(2) 21 | }) 22 | 23 | it('could should handle database errors', async () => { 24 | jest 25 | .spyOn(User.db, 'get') 26 | .mockImplementation((...params) => { 27 | params[params.length - 1]('DB Error!') // callback('DB Error!') 28 | }) 29 | 30 | expect(User.count()).rejects.toEqual('DB Error!') 31 | }) 32 | 33 | it('fetchAttrsApi should retrieve data from the production CryptoKitties API', async () => { 34 | axios.get = jest.fn().mockReturnValue({ 35 | data: { 36 | address: '0x40d7c5fbc89c0ca3880d79e38e5099d786f4ab65', 37 | nickname: 'Eugene', 38 | image: '19', 39 | }, 40 | }) 41 | 42 | const attrs = await User.fetchAttrsApi('0x40D7c5fbc89c0ca3880d79e38e5099d786f4ab65') 43 | expect(attrs).toEqual({ 44 | address: '0x40d7c5fbc89c0ca3880d79e38e5099d786f4ab65', 45 | nickname: 'Eugene', 46 | image: '19', 47 | }) 48 | expect(axios.get).toHaveBeenCalledTimes(1) 49 | expect(axios.get).toHaveBeenCalledWith('https://api.cryptokitties.co/user/0x40D7c5fbc89c0ca3880d79e38e5099d786f4ab65') 50 | }) 51 | 52 | it('findAll should return all users in database', async () => { 53 | const users = await User.findAll() 54 | expect(users.length).toBe(2) 55 | }) 56 | 57 | it('findAll should handle database errors', async () => { 58 | jest 59 | .spyOn(User.db, 'all') 60 | .mockImplementation((...params) => { 61 | params[params.length - 1]('DB Error!') // callback('DB Error!') 62 | }) 63 | 64 | expect(User.findAll()).rejects.toEqual('DB Error!') 65 | }) 66 | 67 | it('findByAddress should return user associated with given address', async () => { 68 | const user = await User.findByAddress('0x456') 69 | expect(user.api_object).toBeDefined() 70 | }) 71 | 72 | it('findByAddress should handles database errors', async () => { 73 | jest 74 | .spyOn(User.db, 'get') 75 | .mockImplementation((...params) => { 76 | params[params.length - 1]('DB Error!') // callback('DB Error!') 77 | }) 78 | 79 | expect(User.findByAddress('0x123')).rejects.toEqual('DB Error!') 80 | }) 81 | 82 | it('findByAddress should return undefined when user with address is not found', async () => { 83 | const user = await User.findByAddress('0xabc') 84 | expect(user).toBeUndefined() 85 | }) 86 | 87 | it('createUser should insert user into database', async () => { 88 | await User.createUser('0x111', '0x222', { name: 'jonno' }) 89 | const user = await User.findByAddress('0x222') 90 | expect(JSON.parse(user.api_object).name).toBe('jonno') 91 | }) 92 | 93 | it('createUser should handle database errors', async () => { 94 | jest 95 | .spyOn(User.db, 'run') 96 | .mockImplementation((...params) => { 97 | params[params.length - 1]('DB Error!') // callback('DB Error!') 98 | }) 99 | 100 | await expect(User.createUser('0x111', '0x222', { name: 'jonno' })).rejects.toEqual('DB Error!') 101 | }) 102 | 103 | it('importUser should create user with production API data', async () => { 104 | axios.get = jest.fn() 105 | .mockReturnValueOnce({ // /user/0x111 106 | data: { 107 | address: '0x111', 108 | nickname: 'Jonno', 109 | image: '19', 110 | }, 111 | }) 112 | .mockReturnValueOnce({ // /kitties?owner_wallet_address=0x111 113 | data: { 114 | kitties: [], 115 | }, 116 | }) 117 | 118 | Kitty.importKitty = jest.fn().mockResolvedValue(23) 119 | 120 | const addressTestnet = await User.importUser('0x111', '0x456') 121 | expect(addressTestnet).toBe('0x456') 122 | 123 | const user = await User.findByAddress(addressTestnet) 124 | expect(JSON.parse(user.api_object).nickname).toBe('Jonno') 125 | 126 | expect(Kitty.importKitty).toHaveBeenCalledTimes(0) 127 | }) 128 | 129 | it('importUser should create user and kitties with production API data', async () => { 130 | axios.get = jest.fn() 131 | .mockReturnValueOnce({ // GET /user/0x111 132 | data: { 133 | address: '0x111', 134 | nickname: 'Jonno', 135 | image: '19', 136 | }, 137 | }) 138 | .mockReturnValueOnce({ // GET /kitties?owner_wallet_address=0x111 139 | data: { 140 | kitties: [ 141 | { 142 | id: 123, 143 | name: 'Cheshire', 144 | owner: { 145 | address: '0x111', 146 | }, 147 | }, 148 | ], 149 | }, 150 | }) 151 | 152 | Kitty.importKitty = jest.fn().mockResolvedValue(23) 153 | 154 | const addressTestnet = await User.importUser('0x111', '0x456') 155 | expect(addressTestnet).toBe('0x456') 156 | 157 | const user = await User.findByAddress(addressTestnet) 158 | expect(JSON.parse(user.api_object).nickname).toBe('Jonno') 159 | expect(Kitty.importKitty).toHaveBeenCalledTimes(1) 160 | expect(Kitty.importKitty).toHaveBeenCalledWith(123, '0x456') 161 | }) 162 | }) 163 | -------------------------------------------------------------------------------- /test-cheshire/kitty.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const Contract = require('../lib/contract.js') 4 | const Kitty = require('../lib/kitty.js') 5 | 6 | describe('kitty', () => { 7 | beforeEach(() => { 8 | Kitty.db.serialize(() => { 9 | Kitty.db.run('DELETE FROM kitties') 10 | Kitty.db.run('INSERT INTO kitties (id_mainnet, id_testnet, owner, api_object) VALUES (?, ?, ?, ?)', 1, 1, '0x123', JSON.stringify({ name: 'Snowball I' })) 11 | Kitty.db.run('INSERT INTO kitties (id_mainnet, id_testnet, owner, api_object) VALUES (?, ?, ?, ?)', 2, 2, '0x123', JSON.stringify({ name: 'Snowball II' })) 12 | Kitty.db.run('INSERT INTO kitties (id_mainnet, id_testnet, owner, api_object) VALUES (?, ?, ?, ?)', 3, 3, '0x123', JSON.stringify({ name: 'Snowball III' })) 13 | Kitty.db.run('INSERT INTO kitties (id_mainnet, id_testnet, owner, api_object) VALUES (?, ?, ?, ?)', 4, 4, '0x444', JSON.stringify({ name: 'Furball I' })) 14 | }) 15 | }) 16 | 17 | afterAll(() => { 18 | Kitty.db.close() 19 | }) 20 | 21 | it('count should return total number of kitties in database', async () => { 22 | const count = await Kitty.count() 23 | expect(count).toBe(4) 24 | }) 25 | 26 | it('count should return total number of kitties owned by address', async () => { 27 | const count = await Kitty.count('0x123') 28 | expect(count).toBe(3) 29 | }) 30 | 31 | it('count should handle database errors', async () => { 32 | jest 33 | .spyOn(Kitty.db, 'get') 34 | .mockImplementation((...params) => { 35 | params[params.length - 1]('DB Error!') // callback('DB Error!') 36 | }) 37 | 38 | expect(Kitty.count()).rejects.toEqual('DB Error!') 39 | }) 40 | 41 | it('fetchAttrsApi should retrieve data from the production CryptoKitties API', async () => { 42 | axios.get = jest.fn().mockReturnValue({ 43 | data: { 44 | id: 123, 45 | name: 'Cheshire', 46 | }, 47 | }) 48 | 49 | const attrs = await Kitty.fetchAttrsApi(123) 50 | expect(attrs).toEqual({ 51 | id: 123, 52 | name: 'Cheshire', 53 | }) 54 | expect(axios.get).toHaveBeenCalledTimes(1) 55 | expect(axios.get).toHaveBeenCalledWith('https://api.cryptokitties.co/kitties/123') 56 | }) 57 | 58 | it('fetchAttrsChain should retrieve data from the production KittyCore contract', async () => { 59 | const getKittyMock = jest.fn().mockReturnValue([ 60 | false, // isGestating 61 | true, // isReady 62 | 0, // cooldownIndex 63 | 0, // nextActionAt 64 | 0, // siringWithId 65 | 1511111111, // birthTime 66 | 0, // matronId 67 | 0, // sireId 68 | 3, // generation 69 | 123456789, // genes 70 | ]) 71 | Contract.declarationMainnet = jest.fn().mockReturnValue({ 72 | at: jest.fn().mockReturnValue({ 73 | getKitty: getKittyMock, 74 | }), 75 | }) 76 | 77 | const attrs = await Kitty.fetchAttrsChain(123) 78 | 79 | expect(attrs).toEqual({ 80 | birthTime: 1511111111, 81 | cooldownIndex: 0, 82 | generation: 3, 83 | genes: 123456789, 84 | isGestating: false, 85 | isReady: true, 86 | matronId: 0, 87 | nextActionAt: 0, 88 | sireId: 0, 89 | siringWithId: 0, 90 | }) 91 | expect(getKittyMock).toHaveBeenCalledTimes(1) 92 | expect(getKittyMock).toHaveBeenCalledWith(123) 93 | }) 94 | 95 | it('findAll should return all kitties associated with an address', async () => { 96 | const kitties = await Kitty.findAll('0x123', 12, 0) 97 | expect(kitties.length).toBe(3) 98 | expect(kitties[0].api_object).toBeDefined() 99 | }) 100 | 101 | it('findAll should handle database errors', async () => { 102 | jest 103 | .spyOn(Kitty.db, 'all') 104 | .mockImplementation((...params) => { 105 | params[params.length - 1]('DB Error!') // callback('DB Error!') 106 | }) 107 | 108 | expect(Kitty.findAll('0x123', 12, 0)).rejects.toEqual('DB Error!') 109 | }) 110 | 111 | it('findAll should return an empty array when no kitties are found', async () => { 112 | const kitties = await Kitty.findAll('0x234', 12, 0) 113 | expect(kitties.length).toBe(0) 114 | }) 115 | 116 | it('findAll should return all kitties when no owner filter is given', async () => { 117 | const kitties = await Kitty.findAll(null, 12, 0) 118 | expect(kitties.length).toBe(4) 119 | }) 120 | 121 | it('findById should return a kitty given its ID', async () => { 122 | const kitty = await Kitty.findById(1) 123 | expect(kitty.api_object).toBeDefined() 124 | }) 125 | 126 | it('findById should return undefined when a kitty matching ID does not exist', async () => { 127 | const kitty = await Kitty.findById(999) 128 | expect(kitty).toBeUndefined() 129 | }) 130 | 131 | it('findById should handle database errors', async () => { 132 | jest 133 | .spyOn(Kitty.db, 'get') 134 | .mockImplementation((...params) => { 135 | params[params.length - 1]('DB Error!') // callback('DB Error!') 136 | }) 137 | 138 | expect(Kitty.findById(123)).rejects.toEqual('DB Error!') 139 | }) 140 | 141 | it('createKitty should add a kitty to testnet KittyCore and the local database', async () => { 142 | const coreCreateKittyMock = jest.fn().mockReturnValue(23) 143 | Contract.declaration = jest.fn().mockReturnValue({ 144 | at: jest.fn().mockReturnValue({ 145 | createKitty: coreCreateKittyMock, 146 | }), 147 | }) 148 | 149 | const kittyId = await Kitty.createKitty( 150 | 1, // matron ID 151 | 2, // sire ID 152 | 3, // generation 153 | 123456789, // genes 154 | '0x123', // owner 155 | { name: 'cheshire', owner: { address: '0x123' } }, // apiObject 156 | ) 157 | 158 | expect(kittyId).toBe(23) 159 | expect(coreCreateKittyMock).toHaveBeenCalledTimes(2) 160 | expect(coreCreateKittyMock).toHaveBeenCalledWith( 161 | 1, // matron ID 162 | 2, // sire ID 163 | 3, // generation 164 | 123456789, // genes 165 | '0x123', // owner 166 | { 167 | gas: 500000, 168 | gasPrice: 10000000000, 169 | value: 0, 170 | }, 171 | ) 172 | 173 | const kitty = await Kitty.findById(kittyId) 174 | expect(kitty.owner).toBe('0x123') 175 | expect(JSON.parse(kitty.api_object).owner.address).toBe('0x123') 176 | }) 177 | 178 | it('createKitty should add an owner attribute to apiObject if missing', async () => { 179 | const coreCreateKittyMock = jest.fn().mockReturnValue(23) 180 | Contract.declaration = jest.fn().mockReturnValue({ 181 | at: jest.fn().mockReturnValue({ 182 | createKitty: coreCreateKittyMock, 183 | }), 184 | }) 185 | 186 | const kittyId = await Kitty.createKitty( 187 | 1, // matron ID 188 | 2, // sire ID 189 | 3, // generation 190 | 123456789, // genes 191 | '0x123', // owner 192 | { name: 'cheshire' }, // apiObject 193 | ) 194 | 195 | expect(kittyId).toBe(23) 196 | expect(coreCreateKittyMock).toHaveBeenCalledTimes(2) 197 | expect(coreCreateKittyMock).toHaveBeenCalledWith( 198 | 1, // matron ID 199 | 2, // sire ID 200 | 3, // generation 201 | 123456789, // genes 202 | '0x123', // owner 203 | { 204 | gas: 500000, 205 | gasPrice: 10000000000, 206 | value: 0, 207 | }, 208 | ) 209 | 210 | const kitty = await Kitty.findById(kittyId) 211 | expect(kitty.owner).toBe('0x123') 212 | expect(JSON.parse(kitty.api_object).owner.address).toBe('0x123') 213 | }) 214 | 215 | it('createKitty should handle database errors', async () => { 216 | const coreCreateKittyMock = jest.fn().mockReturnValue(4) 217 | Contract.declaration = jest.fn().mockReturnValue({ 218 | at: jest.fn().mockReturnValue({ 219 | createKitty: coreCreateKittyMock, 220 | }), 221 | }) 222 | 223 | jest 224 | .spyOn(Kitty.db, 'run') 225 | .mockImplementation((...params) => { 226 | params[params.length - 1]('DB Error!') // callback('DB Error!') 227 | }) 228 | 229 | await expect(Kitty.createKitty( 230 | 1, // matron ID 231 | 2, // sire ID 232 | 3, // generation 233 | 123456789, // genes 234 | '0x123', // owner 235 | { name: 'cheshire' }, // attrs 236 | )).rejects.toEqual('DB Error!') 237 | }) 238 | 239 | it('importKitty should create kitty with mainnet/production KittyCore and API data', async () => { 240 | Kitty.fetchAttrsChain = jest.fn().mockResolvedValueOnce({ 241 | isGestating: false, 242 | isReady: true, 243 | cooldownIndex: 0, 244 | nextActionAt: 0, 245 | siringWithId: 0, 246 | birthTime: 1511111111, 247 | matronId: 1, 248 | sireId: 2, 249 | generation: 3, 250 | genes: 123456789, 251 | }) 252 | 253 | Kitty.fetchAttrsApi = jest.fn().mockResolvedValueOnce({ 254 | id: 123, 255 | name: 'Cheshire', 256 | owner: { 257 | address: '0x111', 258 | }, 259 | }) 260 | 261 | Kitty.createKitty = jest.fn().mockResolvedValue(4) 262 | 263 | const kittyId = await Kitty.importKitty(1, '0x111') 264 | expect(kittyId).toBe(4) 265 | expect(Kitty.createKitty).toHaveBeenCalledTimes(1) 266 | expect(Kitty.createKitty).toHaveBeenCalledWith( 267 | 1, // matron ID 268 | 2, // sire ID 269 | 3, // generation 270 | 123456789, // genes 271 | '0x111', // owner 272 | { 273 | id: 123, 274 | name: 'Cheshire', 275 | owner: { 276 | address: '0x111', 277 | }, 278 | }, 279 | ) 280 | }) 281 | }) 282 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/endless-nameless-inc/cheshire/tree/master.svg?style=shield)](https://circleci.com/gh/endless-nameless-inc/cheshire/tree/master) 2 | [![Coverage Status](https://coveralls.io/repos/github/endless-nameless-inc/cheshire/badge.svg?branch=master)](https://coveralls.io/github/endless-nameless-inc/cheshire?branch=master) 3 | 4 | # Cheshire 5 | 6 | Cheshire enables fast CryptoKitties dApp development by providing local implementations of the CryptoKitties web API and smart contracts. It features: 7 | 8 | 1. An **Ethereum testnet** running the CryptoKitties smart contracts 9 | 10 | 2. An HTTP server running a **minimal implementation of the CryptoKitties web API**: 11 | * `/kitties` 12 | * `/kitties/:id` 13 | * `/user/:address` 14 | 15 | 3. A simple **Node.js framework** for seeding the development environment with realistic data and bootstraping your dApp 16 | 17 | Cheshire has simplified and accelerated development at [Endless Nameless](http://endlessnameless.com) considerably. We're excited to share it. 18 | 19 | ## Installation 20 | 21 | You can install Cheshire with git or as a [Truffle Box](http://truffleframework.com/boxes/). 22 | 23 | ### Git 24 | 25 | ```bash 26 | git clone http://github.com/endless-nameless-inc/cheshire 27 | cd cheshire 28 | yarn install 29 | ``` 30 | 31 | ### Truffle box 32 | 33 | ```bash 34 | truffle unbox endless-nameless-inc/cheshire 35 | ``` 36 | 37 | ## Usage 38 | 39 | Cheshire is meant to be used with the [Truffle Framework](http://truffleframework.com/), but can function as a standalone service, depending on your workflow. 40 | 41 | ### Start Cheshire 42 | 43 | To start Cheshire, run: 44 | 45 | `yarn start` 46 | 47 | This does the following: 48 | 49 | 1. Starts an Ethereum testnet ([ganache-cli](https://github.com/trufflesuite/ganache-cli)) 50 | 2. Deploys CryptoKitties's [KittyCore](https://etherscan.io/address/0x06012c8cf97bead5deae237070f9587f8e7a266d#code), [SaleClockAuction](https://etherscan.io/address/0xb1690c08e213a35ed9bab7b318de14420fb57d8c#code), and [SiringClockAuction](https://etherscan.io/address/0xc7af99fe5513eb6710e6d5f44f9989da40f27f26#code) contracts to the testnet 51 | 3. Starts a local CryptoKitties API server 52 | 4. Executes `/scripts/setup.js` 53 | 54 | The output should look something like this: 55 | 56 | ``` 57 | > Starting database... 58 | > Starting testnet... 59 | > Compiling contracts... 60 | > Deploying CryptoKitties contracts to testnet... 61 | > Starting local CryptoKitties API server... 62 | > Running setup script... 63 | 64 | Cheshire is live 😺 Here's what's inside: 65 | 66 | Available Accounts 67 | ==================== 68 | (0) 0x182fc09c33fdd6c2f5b2562f3ca721fa954689c8 69 | ... 70 | (9) 0xcdf40e926a778d93429b72c341b4a9e0ee8624c4 71 | 72 | Private Keys 73 | ==================== 74 | (0) 0x76a67ae288fd67ea8d4f7fb94f50c36b606d9448db579584af90d52105f9d8cf 75 | ... 76 | (9) 0x6e77cfded732de6d423abcaccc45ee8c4bdc2eb3c0c47938acb386ac17c496b8 77 | 78 | Testnet Contracts 79 | ==================== 80 | KittyCore: 0xa751b62893867d0608a2ada5d17d0c43e3433040 81 | SaleClockAuction: 0x1ab49d53d0bff0202ec4b330349b427155bba7ac 82 | SiringClockAuction: 0x671843106e07f9d835d7299381cd14863af18593 83 | 84 | Services 85 | ==================== 86 | Ethereum testnet listening on port 8546 87 | CryptoKitties API listening on port 4000 88 | Cheshire dashboard available at http://localhost:4000 89 | 90 | View the above at any time by running `yarn run help` 91 | ``` 92 | 93 | Eureka! When Cheshire's running, you have your very own local copy of CryptoKitties, enabling you to build your dApp with the speed and convenience of testnet. Let's try it out. 94 | 95 | ### Interacting with your local CryptoKitties API 96 | 97 | Cheshire automatically imports the Genesis kitty. To fetch the Genesis kitty from your local CryptoKitties API, run: 98 | 99 | ```bash 100 | $ curl http://localhost:4000/kitties/1 101 | ``` 102 | 103 | The response should look exactly like the response returned by CryptoKitties's [production API](https://api.cryptokitties.co/kitties/1). 104 | 105 | See the [scripts](#scripts) section below to learn how to seed your environment with more data. 106 | 107 | ### Interacting with the testnet contracts 108 | 109 | To interact with the testnet contracts, start by opening a Truffle console: 110 | 111 | ```bash 112 | $ truffle console --network cheshire 113 | ``` 114 | 115 | Then, taking note of the KittyCore testnet address displayed when you started Cheshire, create an instance of KittyCore, and use the `getKitty` function to fetch the Genesis kitty's genes: 116 | 117 | ```bash 118 | truffle(cheshire)> // Be sure to replace the KittyCore address below 119 | truffle(cheshire)> kittyCore = KittyCore.at('0xa751b62893867d0608a2ada5d17d0c43e3433040') 120 | truffle(cheshire)> kittyCore.getKitty(1) 121 | ``` 122 | 123 | The response should be pretty similar to the one you get from the [mainnet contract](https://etherscan.io/address/0x06012c8cf97bead5deae237070f9587f8e7a266d#readContract). 124 | 125 | ## Suggested Conventions 126 | 127 | You'll get the most out of Cheshire by adopting these conventions: 128 | 129 | * Store your contracts in the `/contracts` directory 130 | * Design the web application layers of your stack to reference Cheshire's [environment variables](#cheshire-environment-variables) (hat tip to the [twelve-factor](https://12factor.net) methodology) 131 | * Update your [setup script](#setup-script) to deploy your contracts to testnet 132 | * Update your [setup script](#setup-script) to start your dApp's web application 133 | 134 | ## Scripts 135 | 136 | Cheshire provides a simple scripting framework designed to help seed the development environment with realistic data, primarily by importing kitties from mainnet. 137 | 138 | A Cheshire script is just a Node.js module that runs in the context of the Cheshire environment. 139 | 140 | Here's an example of a script that imports a [Bug Cat](https://www.cryptokitties.co/kitty/101) from mainnet to your testnet. 141 | 142 | ```js 143 | // /scripts/import-bug-cat.js 144 | module.exports = async function importBugCat(cheshire) { 145 | const bugCatIdMainnet = 101 146 | const ownerTestnet = cheshire.accounts[0].address 147 | const kittyIdTestnet = await cheshire.importKitty(bugCatIdMainnet, ownerTestnet) 148 | 149 | console.log(`Kitty #${kittyIdTestnet} => ${ownerTestnet}`) 150 | } 151 | ``` 152 | 153 | To run this script, you would execute the following command: 154 | 155 | ```sh 156 | $ yarn run script ./scripts/import-bug-cat.js 157 | ``` 158 | 159 | The output would look something like: 160 | 161 | ```txt 162 | Kitty #2 => 0x182fc09c33fdd6c2f5b2562f3ca721fa954689c8 163 | ``` 164 | 165 | ### Setup Script 166 | 167 | Cheshire executes `/scripts/setup.js` when started. You should update the `setup.js` shipped with Cheshire to: 168 | 169 | 1. Deploy your dApp's contracts to testnet. For example: 170 | 171 | ``` 172 | const kittyRace = await cheshire.deployContract('KittyRace', process.env.ADDRESS_KITTY_CORE) 173 | log('KittyRace deployed at:', kittyRace.address) 174 | ``` 175 | 176 | 2. Start your dApp's web application, so it inherits the various [environment variables](#cheshire-environment-variables) set by Cheshire. 177 | 178 | We recommend adopting the convention in the `setup.js` shipped with Cheshire which simply expects the `APP_START` environment variable to contain a command that starts your dApp's web application. 179 | 180 | For example: 181 | 182 | ``` 183 | APP_START="cd ~/Projects/kittyrace-web; bundle exec rails server" yarn start 184 | ``` 185 | 186 | You can run any script in place of `setup.js` by passing its path to `yarn start`. This is handy for setting up specific scenarios, such as a KittyRace with 9 registered racers: 187 | 188 | ```sh 189 | yarn start ./scripts/setup-registered-racers.js 9 190 | ``` 191 | 192 | ### Cheshire API Reference 193 | 194 | Cheshire scripts receive an instance of the Cheshire class with these methods: 195 | 196 | #### `accounts()` 197 | Returns array of available Ethereum accounts (the same accounts defined in config.json) 198 | 199 | #### `contractAddress(contractName)` 200 | Returns address of `contractName` 201 | 202 | #### `contractInstance(contractName)` 203 | Returns an instance of `contractName` as a `web3.eth.contract` object 204 | 205 | #### `createKitty(matronId, sireId, generation, genes, owner, apiObject)` 206 | Create a kitty with the given parameters. 207 | 208 | Returns the kitty's ID. 209 | 210 | #### `async deployContract(contractName, ...constructorArgs)` 211 | Deploy `contractName` to testnet. 212 | 213 | Cheshire compiles all contracts in `/contracts` at start time. Expects `/contracts/ContractName.sol` to exist. 214 | 215 | Returns an instance of `contractName` as a `web3.eth.contract` object 216 | 217 | #### `async importKitty(kittyIdMainnet, ownerTestnet)` 218 | Import a kitty from mainnet, and assign it to `ownerTestnet` 219 | 220 | Returns the testnet kitty's ID. 221 | 222 | #### `async importUser(addressMainnet, addressTestnet)` 223 | Import user's profile and kitties from mainnet, and assign to `addressTestnet`. 224 | 225 | Returns address of testnet user. 226 | 227 | ### Cheshire Environment Variables 228 | 229 | Cheshire sets several environment variables before running any script: 230 | 231 | * `ADDRESS_KITTY_CORE` 232 | * `ADDRESS_SALE_CLOCK_AUCTION` 233 | * `ADDRESS_SIRING_CLOCK_AUCTION` 234 | * `URL_CRYPTO_KITTIES_API` 235 | 236 | In addition to these, the address for any contract deployed with a Cheshire script will be stored in an environment variable named with the convention, `ADDRESS_`. 237 | 238 | ## Configuration 239 | 240 | The `config.json` file defines the following: 241 | 242 | * `accounts` - list of Ethereum accounts to load into testnet 243 | * `ethNodeMainnet` - URL for the node used to access the Ethereum mainnet 244 | * `addressKittyCoreMainnet` - address of the mainnet KittyCore contract 245 | * `portTestnet` - port bound by Ethereum testnet 246 | * `portApi` - port bound by local CryptoKitties API 247 | 248 | ## Utilities 249 | 250 | ### Mine 251 | To mine some number of blocks on your testnet: 252 | 253 | `yarn run mine ` 254 | 255 | ### Help 256 | Print information about the environment including available Ethereum accounts, contract addresses, etc. 257 | 258 | `yarn run help` 259 | 260 | ### Cheshire Dashboard 261 | Cheshire ships with a simple dashboard you can access at [http://localhost:4000](http://localhost:4000) 262 | 263 | ## Developer notes 264 | 265 | ### KittyCore 266 | 267 | The smart contracts bundled with Cheshire are identical to those in production except for KittyCore, to which we've added an `external` `createKitty` function that lets us push kitties into the local testnet contract. 268 | 269 | ```solidity 270 | function createKitty( 271 | uint256 _matronId, 272 | uint256 _sireId, 273 | uint256 _generation, 274 | uint256 _genes, 275 | address _owner 276 | ) 277 | external 278 | returns (uint) 279 | { 280 | return _createKitty(_matronId, _sireId, _generation, _genes, _owner); 281 | } 282 | ``` 283 | 284 | ### Contributions 285 | 286 | Cheshire works pretty well for us at [Endless Nameless](http://endlessnameless.com), but there's probably a whole lot more it could do! 287 | 288 | If you're interested in contributing, we humbly request the following: 289 | 290 | 1. Adhere to Airbnb's [JavaScript style guide](https://github.com/airbnb/javascript) (`yarn eslint` makes it easy) 291 | 292 | 2. Include tests. We're happy when `yarn test` is happy, and `yarn test` is only happy when coverage is 100% 🤓 293 | 294 | ## Acknowledgements 295 | 296 | We're grateful for the contributions of the many open source projects on which Cheshire depends, none more so than the excellent [Truffle Suite](https://github.com/trufflesuite/). 297 | 298 | Cheshire is by [Endless Nameless](http://endlessnameless.com). It is based on tools and processes we developed while building [KittyRace](https://kittyrace.com), a CryptoKitties dApp. We hope Cheshire makes it easier to #buidl 🤘 299 | 300 | _Your name here_ - we will gladly review PRs. 301 | -------------------------------------------------------------------------------- /api/static/axios.min.js: -------------------------------------------------------------------------------- 1 | /* axios v0.18.0 | (c) 2018 by Matt Zabriskie */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new s(e),n=i(s.prototype.request,t);return o.extend(n,s.prototype,t),o.extend(n,t),n}var o=n(2),i=n(3),s=n(5),u=n(6),a=r(u);a.Axios=s,a.create=function(e){return r(o.merge(u,e))},a.Cancel=n(23),a.CancelToken=n(24),a.isCancel=n(20),a.all=function(e){return Promise.all(e)},a.spread=n(25),e.exports=a,e.exports.default=a},function(e,t,n){"use strict";function r(e){return"[object Array]"===R.call(e)}function o(e){return"[object ArrayBuffer]"===R.call(e)}function i(e){return"undefined"!=typeof FormData&&e instanceof FormData}function s(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function u(e){return"string"==typeof e}function a(e){return"number"==typeof e}function c(e){return"undefined"==typeof e}function f(e){return null!==e&&"object"==typeof e}function p(e){return"[object Date]"===R.call(e)}function d(e){return"[object File]"===R.call(e)}function l(e){return"[object Blob]"===R.call(e)}function h(e){return"[object Function]"===R.call(e)}function m(e){return f(e)&&h(e.pipe)}function y(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function w(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function g(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function v(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n 6 | * @license MIT 7 | */ 8 | e.exports=function(e){return null!=e&&(n(e)||r(e)||!!e._isBuffer)}},function(e,t,n){"use strict";function r(e){this.defaults=e,this.interceptors={request:new s,response:new s}}var o=n(6),i=n(2),s=n(17),u=n(18);r.prototype.request=function(e){"string"==typeof e&&(e=i.merge({url:arguments[0]},arguments[1])),e=i.merge(o,{method:"get"},this.defaults,e),e.method=e.method.toLowerCase();var t=[u,void 0],n=Promise.resolve(e);for(this.interceptors.request.forEach(function(e){t.unshift(e.fulfilled,e.rejected)}),this.interceptors.response.forEach(function(e){t.push(e.fulfilled,e.rejected)});t.length;)n=n.then(t.shift(),t.shift());return n},i.forEach(["delete","get","head","options"],function(e){r.prototype[e]=function(t,n){return this.request(i.merge(n||{},{method:e,url:t}))}}),i.forEach(["post","put","patch"],function(e){r.prototype[e]=function(t,n,r){return this.request(i.merge(r||{},{method:e,url:t,data:n}))}}),e.exports=r},function(e,t,n){"use strict";function r(e,t){!i.isUndefined(e)&&i.isUndefined(e["Content-Type"])&&(e["Content-Type"]=t)}function o(){var e;return"undefined"!=typeof XMLHttpRequest?e=n(8):"undefined"!=typeof process&&(e=n(8)),e}var i=n(2),s=n(7),u={"Content-Type":"application/x-www-form-urlencoded"},a={adapter:o(),transformRequest:[function(e,t){return s(t,"Content-Type"),i.isFormData(e)||i.isArrayBuffer(e)||i.isBuffer(e)||i.isStream(e)||i.isFile(e)||i.isBlob(e)?e:i.isArrayBufferView(e)?e.buffer:i.isURLSearchParams(e)?(r(t,"application/x-www-form-urlencoded;charset=utf-8"),e.toString()):i.isObject(e)?(r(t,"application/json;charset=utf-8"),JSON.stringify(e)):e}],transformResponse:[function(e){if("string"==typeof e)try{e=JSON.parse(e)}catch(e){}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,validateStatus:function(e){return e>=200&&e<300}};a.headers={common:{Accept:"application/json, text/plain, */*"}},i.forEach(["delete","get","head"],function(e){a.headers[e]={}}),i.forEach(["post","put","patch"],function(e){a.headers[e]=i.merge(u)}),e.exports=a},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(9),i=n(12),s=n(13),u=n(14),a=n(10),c="undefined"!=typeof window&&window.btoa&&window.btoa.bind(window)||n(15);e.exports=function(e){return new Promise(function(t,f){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest,h="onreadystatechange",m=!1;if("undefined"==typeof window||!window.XDomainRequest||"withCredentials"in l||u(e.url)||(l=new window.XDomainRequest,h="onload",m=!0,l.onprogress=function(){},l.ontimeout=function(){}),e.auth){var y=e.auth.username||"",w=e.auth.password||"";d.Authorization="Basic "+c(y+":"+w)}if(l.open(e.method.toUpperCase(),i(e.url,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l[h]=function(){if(l&&(4===l.readyState||m)&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var n="getAllResponseHeaders"in l?s(l.getAllResponseHeaders()):null,r=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:r,status:1223===l.status?204:l.status,statusText:1223===l.status?"No Content":l.statusText,headers:n,config:e,request:l};o(t,f,i),l=null}},l.onerror=function(){f(a("Network Error",e,null,l)),l=null},l.ontimeout=function(){f(a("timeout of "+e.timeout+"ms exceeded",e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=n(16),v=(e.withCredentials||u(e.url))&&e.xsrfCookieName?g.read(e.xsrfCookieName):void 0;v&&(d[e.xsrfHeaderName]=v)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),e.withCredentials&&(l.withCredentials=!0),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),f(e),l=null)}),void 0===p&&(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(10);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},function(e,t,n){"use strict";var r=n(11);e.exports=function(e,t,n,o,i){var s=new Error(e);return r(s,t,n,o,i)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e}},function(e,t,n){"use strict";function r(e){return encodeURIComponent(e).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}var o=n(2);e.exports=function(e,t,n){if(!t)return e;var i;if(n)i=n(t);else if(o.isURLSearchParams(t))i=t.toString();else{var s=[];o.forEach(t,function(e,t){null!==e&&"undefined"!=typeof e&&(o.isArray(e)?t+="[]":e=[e],o.forEach(e,function(e){o.isDate(e)?e=e.toISOString():o.isObject(e)&&(e=JSON.stringify(e)),s.push(r(t)+"="+r(e))}))}),i=s.join("&")}return i&&(e+=(e.indexOf("?")===-1?"?":"&")+i),e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,i,s={};return e?(r.forEach(e.split("\n"),function(e){if(i=e.indexOf(":"),t=r.trim(e.substr(0,i)).toLowerCase(),n=r.trim(e.substr(i+1)),t){if(s[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?s[t]=(s[t]?s[t]:[]).concat([n]):s[t]=s[t]?s[t]+", "+n:n}}),s):s}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t){"use strict";function n(){this.message="String contains an invalid character"}function r(e){for(var t,r,i=String(e),s="",u=0,a=o;i.charAt(0|u)||(a="=",u%1);s+=a.charAt(63&t>>8-u%1*8)){if(r=i.charCodeAt(u+=.75),r>255)throw new n;t=t<<8|r}return s}var o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";n.prototype=new Error,n.prototype.code=5,n.prototype.name="InvalidCharacterError",e.exports=r},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,i,s){var u=[];u.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&u.push("expires="+new Date(n).toGMTString()),r.isString(o)&&u.push("path="+o),r.isString(i)&&u.push("domain="+i),s===!0&&u.push("secure"),document.cookie=u.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";function r(){this.handlers=[]}var o=n(2);r.prototype.use=function(e,t){return this.handlers.push({fulfilled:e,rejected:t}),this.handlers.length-1},r.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},r.prototype.forEach=function(e){o.forEach(this.handlers,function(t){null!==t&&e(t)})},e.exports=r},function(e,t,n){"use strict";function r(e){e.cancelToken&&e.cancelToken.throwIfRequested()}var o=n(2),i=n(19),s=n(20),u=n(6),a=n(21),c=n(22);e.exports=function(e){r(e),e.baseURL&&!a(e.url)&&(e.url=c(e.baseURL,e.url)),e.headers=e.headers||{},e.data=i(e.data,e.headers,e.transformRequest),e.headers=o.merge(e.headers.common||{},e.headers[e.method]||{},e.headers||{}),o.forEach(["delete","get","head","post","put","patch","common"],function(t){delete e.headers[t]});var t=e.adapter||u.adapter;return t(e).then(function(t){return r(e),t.data=i(t.data,t.headers,e.transformResponse),t},function(t){return s(t)||(r(e),t&&t.response&&(t.response.data=i(t.response.data,t.response.headers,e.transformResponse))),Promise.reject(t)})}},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t,n){return r.forEach(n,function(n){e=n(e,t)}),e}},function(e,t){"use strict";e.exports=function(e){return!(!e||!e.__CANCEL__)}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}}])}); 9 | //# sourceMappingURL=axios.min.map -------------------------------------------------------------------------------- /contracts/SiringClockAuction.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | 4 | /** 5 | * @title Ownable 6 | * @dev The Ownable contract has an owner address, and provides basic authorization control 7 | * functions, this simplifies the implementation of "user permissions". 8 | */ 9 | contract Ownable { 10 | address public owner; 11 | 12 | 13 | /** 14 | * @dev The Ownable constructor sets the original `owner` of the contract to the sender 15 | * account. 16 | */ 17 | function Ownable() { 18 | owner = msg.sender; 19 | } 20 | 21 | 22 | /** 23 | * @dev Throws if called by any account other than the owner. 24 | */ 25 | modifier onlyOwner() { 26 | require(msg.sender == owner); 27 | _; 28 | } 29 | 30 | 31 | /** 32 | * @dev Allows the current owner to transfer control of the contract to a newOwner. 33 | * @param newOwner The address to transfer ownership to. 34 | */ 35 | function transferOwnership(address newOwner) onlyOwner { 36 | if (newOwner != address(0)) { 37 | owner = newOwner; 38 | } 39 | } 40 | 41 | } 42 | 43 | 44 | 45 | /// @title Interface for contracts conforming to ERC-721: Non-Fungible Tokens 46 | /// @author Dieter Shirley (https://github.com/dete) 47 | contract ERC721 { 48 | // Required methods 49 | function totalSupply() public view returns (uint256 total); 50 | function balanceOf(address _owner) public view returns (uint256 balance); 51 | function ownerOf(uint256 _tokenId) external view returns (address owner); 52 | function approve(address _to, uint256 _tokenId) external; 53 | function transfer(address _to, uint256 _tokenId) external; 54 | function transferFrom(address _from, address _to, uint256 _tokenId) external; 55 | 56 | // Events 57 | event Transfer(address from, address to, uint256 tokenId); 58 | event Approval(address owner, address approved, uint256 tokenId); 59 | 60 | // Optional 61 | // function name() public view returns (string name); 62 | // function symbol() public view returns (string symbol); 63 | // function tokensOfOwner(address _owner) external view returns (uint256[] tokenIds); 64 | // function tokenMetadata(uint256 _tokenId, string _preferredTransport) public view returns (string infoUrl); 65 | 66 | // ERC-165 Compatibility (https://github.com/ethereum/EIPs/issues/165) 67 | function supportsInterface(bytes4 _interfaceID) external view returns (bool); 68 | } 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | /// @title Auction Core 79 | /// @dev Contains models, variables, and internal methods for the auction. 80 | /// @notice We omit a fallback function to prevent accidental sends to this contract. 81 | contract ClockAuctionBase { 82 | 83 | // Represents an auction on an NFT 84 | struct Auction { 85 | // Current owner of NFT 86 | address seller; 87 | // Price (in wei) at beginning of auction 88 | uint128 startingPrice; 89 | // Price (in wei) at end of auction 90 | uint128 endingPrice; 91 | // Duration (in seconds) of auction 92 | uint64 duration; 93 | // Time when auction started 94 | // NOTE: 0 if this auction has been concluded 95 | uint64 startedAt; 96 | } 97 | 98 | // Reference to contract tracking NFT ownership 99 | ERC721 public nonFungibleContract; 100 | 101 | // Cut owner takes on each auction, measured in basis points (1/100 of a percent). 102 | // Values 0-10,000 map to 0%-100% 103 | uint256 public ownerCut; 104 | 105 | // Map from token ID to their corresponding auction. 106 | mapping (uint256 => Auction) tokenIdToAuction; 107 | 108 | event AuctionCreated(uint256 tokenId, uint256 startingPrice, uint256 endingPrice, uint256 duration); 109 | event AuctionSuccessful(uint256 tokenId, uint256 totalPrice, address winner); 110 | event AuctionCancelled(uint256 tokenId); 111 | 112 | /// @dev Returns true if the claimant owns the token. 113 | /// @param _claimant - Address claiming to own the token. 114 | /// @param _tokenId - ID of token whose ownership to verify. 115 | function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) { 116 | return (nonFungibleContract.ownerOf(_tokenId) == _claimant); 117 | } 118 | 119 | /// @dev Escrows the NFT, assigning ownership to this contract. 120 | /// Throws if the escrow fails. 121 | /// @param _owner - Current owner address of token to escrow. 122 | /// @param _tokenId - ID of token whose approval to verify. 123 | function _escrow(address _owner, uint256 _tokenId) internal { 124 | // it will throw if transfer fails 125 | nonFungibleContract.transferFrom(_owner, this, _tokenId); 126 | } 127 | 128 | /// @dev Transfers an NFT owned by this contract to another address. 129 | /// Returns true if the transfer succeeds. 130 | /// @param _receiver - Address to transfer NFT to. 131 | /// @param _tokenId - ID of token to transfer. 132 | function _transfer(address _receiver, uint256 _tokenId) internal { 133 | // it will throw if transfer fails 134 | nonFungibleContract.transfer(_receiver, _tokenId); 135 | } 136 | 137 | /// @dev Adds an auction to the list of open auctions. Also fires the 138 | /// AuctionCreated event. 139 | /// @param _tokenId The ID of the token to be put on auction. 140 | /// @param _auction Auction to add. 141 | function _addAuction(uint256 _tokenId, Auction _auction) internal { 142 | // Require that all auctions have a duration of 143 | // at least one minute. (Keeps our math from getting hairy!) 144 | require(_auction.duration >= 1 minutes); 145 | 146 | tokenIdToAuction[_tokenId] = _auction; 147 | 148 | AuctionCreated( 149 | uint256(_tokenId), 150 | uint256(_auction.startingPrice), 151 | uint256(_auction.endingPrice), 152 | uint256(_auction.duration) 153 | ); 154 | } 155 | 156 | /// @dev Cancels an auction unconditionally. 157 | function _cancelAuction(uint256 _tokenId, address _seller) internal { 158 | _removeAuction(_tokenId); 159 | _transfer(_seller, _tokenId); 160 | AuctionCancelled(_tokenId); 161 | } 162 | 163 | /// @dev Computes the price and transfers winnings. 164 | /// Does NOT transfer ownership of token. 165 | function _bid(uint256 _tokenId, uint256 _bidAmount) 166 | internal 167 | returns (uint256) 168 | { 169 | // Get a reference to the auction struct 170 | Auction storage auction = tokenIdToAuction[_tokenId]; 171 | 172 | // Explicitly check that this auction is currently live. 173 | // (Because of how Ethereum mappings work, we can't just count 174 | // on the lookup above failing. An invalid _tokenId will just 175 | // return an auction object that is all zeros.) 176 | require(_isOnAuction(auction)); 177 | 178 | // Check that the bid is greater than or equal to the current price 179 | uint256 price = _currentPrice(auction); 180 | require(_bidAmount >= price); 181 | 182 | // Grab a reference to the seller before the auction struct 183 | // gets deleted. 184 | address seller = auction.seller; 185 | 186 | // The bid is good! Remove the auction before sending the fees 187 | // to the sender so we can't have a reentrancy attack. 188 | _removeAuction(_tokenId); 189 | 190 | // Transfer proceeds to seller (if there are any!) 191 | if (price > 0) { 192 | // Calculate the auctioneer's cut. 193 | // (NOTE: _computeCut() is guaranteed to return a 194 | // value <= price, so this subtraction can't go negative.) 195 | uint256 auctioneerCut = _computeCut(price); 196 | uint256 sellerProceeds = price - auctioneerCut; 197 | 198 | // NOTE: Doing a transfer() in the middle of a complex 199 | // method like this is generally discouraged because of 200 | // reentrancy attacks and DoS attacks if the seller is 201 | // a contract with an invalid fallback function. We explicitly 202 | // guard against reentrancy attacks by removing the auction 203 | // before calling transfer(), and the only thing the seller 204 | // can DoS is the sale of their own asset! (And if it's an 205 | // accident, they can call cancelAuction(). ) 206 | seller.transfer(sellerProceeds); 207 | } 208 | 209 | // Calculate any excess funds included with the bid. If the excess 210 | // is anything worth worrying about, transfer it back to bidder. 211 | // NOTE: We checked above that the bid amount is greater than or 212 | // equal to the price so this cannot underflow. 213 | uint256 bidExcess = _bidAmount - price; 214 | 215 | // Return the funds. Similar to the previous transfer, this is 216 | // not susceptible to a re-entry attack because the auction is 217 | // removed before any transfers occur. 218 | msg.sender.transfer(bidExcess); 219 | 220 | // Tell the world! 221 | AuctionSuccessful(_tokenId, price, msg.sender); 222 | 223 | return price; 224 | } 225 | 226 | /// @dev Removes an auction from the list of open auctions. 227 | /// @param _tokenId - ID of NFT on auction. 228 | function _removeAuction(uint256 _tokenId) internal { 229 | delete tokenIdToAuction[_tokenId]; 230 | } 231 | 232 | /// @dev Returns true if the NFT is on auction. 233 | /// @param _auction - Auction to check. 234 | function _isOnAuction(Auction storage _auction) internal view returns (bool) { 235 | return (_auction.startedAt > 0); 236 | } 237 | 238 | /// @dev Returns current price of an NFT on auction. Broken into two 239 | /// functions (this one, that computes the duration from the auction 240 | /// structure, and the other that does the price computation) so we 241 | /// can easily test that the price computation works correctly. 242 | function _currentPrice(Auction storage _auction) 243 | internal 244 | view 245 | returns (uint256) 246 | { 247 | uint256 secondsPassed = 0; 248 | 249 | // A bit of insurance against negative values (or wraparound). 250 | // Probably not necessary (since Ethereum guarnatees that the 251 | // now variable doesn't ever go backwards). 252 | if (now > _auction.startedAt) { 253 | secondsPassed = now - _auction.startedAt; 254 | } 255 | 256 | return _computeCurrentPrice( 257 | _auction.startingPrice, 258 | _auction.endingPrice, 259 | _auction.duration, 260 | secondsPassed 261 | ); 262 | } 263 | 264 | /// @dev Computes the current price of an auction. Factored out 265 | /// from _currentPrice so we can run extensive unit tests. 266 | /// When testing, make this function public and turn on 267 | /// `Current price computation` test suite. 268 | function _computeCurrentPrice( 269 | uint256 _startingPrice, 270 | uint256 _endingPrice, 271 | uint256 _duration, 272 | uint256 _secondsPassed 273 | ) 274 | internal 275 | pure 276 | returns (uint256) 277 | { 278 | // NOTE: We don't use SafeMath (or similar) in this function because 279 | // all of our public functions carefully cap the maximum values for 280 | // time (at 64-bits) and currency (at 128-bits). _duration is 281 | // also known to be non-zero (see the require() statement in 282 | // _addAuction()) 283 | if (_secondsPassed >= _duration) { 284 | // We've reached the end of the dynamic pricing portion 285 | // of the auction, just return the end price. 286 | return _endingPrice; 287 | } else { 288 | // Starting price can be higher than ending price (and often is!), so 289 | // this delta can be negative. 290 | int256 totalPriceChange = int256(_endingPrice) - int256(_startingPrice); 291 | 292 | // This multiplication can't overflow, _secondsPassed will easily fit within 293 | // 64-bits, and totalPriceChange will easily fit within 128-bits, their product 294 | // will always fit within 256-bits. 295 | int256 currentPriceChange = totalPriceChange * int256(_secondsPassed) / int256(_duration); 296 | 297 | // currentPriceChange can be negative, but if so, will have a magnitude 298 | // less that _startingPrice. Thus, this result will always end up positive. 299 | int256 currentPrice = int256(_startingPrice) + currentPriceChange; 300 | 301 | return uint256(currentPrice); 302 | } 303 | } 304 | 305 | /// @dev Computes owner's cut of a sale. 306 | /// @param _price - Sale price of NFT. 307 | function _computeCut(uint256 _price) internal view returns (uint256) { 308 | // NOTE: We don't use SafeMath (or similar) in this function because 309 | // all of our entry functions carefully cap the maximum values for 310 | // currency (at 128-bits), and ownerCut <= 10000 (see the require() 311 | // statement in the ClockAuction constructor). The result of this 312 | // function is always guaranteed to be <= _price. 313 | return _price * ownerCut / 10000; 314 | } 315 | 316 | } 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | /** 325 | * @title Pausable 326 | * @dev Base contract which allows children to implement an emergency stop mechanism. 327 | */ 328 | contract Pausable is Ownable { 329 | event Pause(); 330 | event Unpause(); 331 | 332 | bool public paused = false; 333 | 334 | 335 | /** 336 | * @dev modifier to allow actions only when the contract IS paused 337 | */ 338 | modifier whenNotPaused() { 339 | require(!paused); 340 | _; 341 | } 342 | 343 | /** 344 | * @dev modifier to allow actions only when the contract IS NOT paused 345 | */ 346 | modifier whenPaused { 347 | require(paused); 348 | _; 349 | } 350 | 351 | /** 352 | * @dev called by the owner to pause, triggers stopped state 353 | */ 354 | function pause() onlyOwner whenNotPaused returns (bool) { 355 | paused = true; 356 | Pause(); 357 | return true; 358 | } 359 | 360 | /** 361 | * @dev called by the owner to unpause, returns to normal state 362 | */ 363 | function unpause() onlyOwner whenPaused returns (bool) { 364 | paused = false; 365 | Unpause(); 366 | return true; 367 | } 368 | } 369 | 370 | 371 | /// @title Clock auction for non-fungible tokens. 372 | /// @notice We omit a fallback function to prevent accidental sends to this contract. 373 | contract ClockAuction is Pausable, ClockAuctionBase { 374 | 375 | /// @dev The ERC-165 interface signature for ERC-721. 376 | /// Ref: https://github.com/ethereum/EIPs/issues/165 377 | /// Ref: https://github.com/ethereum/EIPs/issues/721 378 | bytes4 constant InterfaceSignature_ERC721 = bytes4(0x9a20483d); 379 | 380 | /// @dev Constructor creates a reference to the NFT ownership contract 381 | /// and verifies the owner cut is in the valid range. 382 | /// @param _nftAddress - address of a deployed contract implementing 383 | /// the Nonfungible Interface. 384 | /// @param _cut - percent cut the owner takes on each auction, must be 385 | /// between 0-10,000. 386 | function ClockAuction(address _nftAddress, uint256 _cut) public { 387 | require(_cut <= 10000); 388 | ownerCut = _cut; 389 | 390 | ERC721 candidateContract = ERC721(_nftAddress); 391 | require(candidateContract.supportsInterface(InterfaceSignature_ERC721)); 392 | nonFungibleContract = candidateContract; 393 | } 394 | 395 | /// @dev Remove all Ether from the contract, which is the owner's cuts 396 | /// as well as any Ether sent directly to the contract address. 397 | /// Always transfers to the NFT contract, but can be called either by 398 | /// the owner or the NFT contract. 399 | function withdrawBalance() external { 400 | address nftAddress = address(nonFungibleContract); 401 | 402 | require( 403 | msg.sender == owner || 404 | msg.sender == nftAddress 405 | ); 406 | // We are using this boolean method to make sure that even if one fails it will still work 407 | bool res = nftAddress.send(this.balance); 408 | } 409 | 410 | /// @dev Creates and begins a new auction. 411 | /// @param _tokenId - ID of token to auction, sender must be owner. 412 | /// @param _startingPrice - Price of item (in wei) at beginning of auction. 413 | /// @param _endingPrice - Price of item (in wei) at end of auction. 414 | /// @param _duration - Length of time to move between starting 415 | /// price and ending price (in seconds). 416 | /// @param _seller - Seller, if not the message sender 417 | function createAuction( 418 | uint256 _tokenId, 419 | uint256 _startingPrice, 420 | uint256 _endingPrice, 421 | uint256 _duration, 422 | address _seller 423 | ) 424 | external 425 | whenNotPaused 426 | { 427 | // Sanity check that no inputs overflow how many bits we've allocated 428 | // to store them in the auction struct. 429 | require(_startingPrice == uint256(uint128(_startingPrice))); 430 | require(_endingPrice == uint256(uint128(_endingPrice))); 431 | require(_duration == uint256(uint64(_duration))); 432 | 433 | require(_owns(msg.sender, _tokenId)); 434 | _escrow(msg.sender, _tokenId); 435 | Auction memory auction = Auction( 436 | _seller, 437 | uint128(_startingPrice), 438 | uint128(_endingPrice), 439 | uint64(_duration), 440 | uint64(now) 441 | ); 442 | _addAuction(_tokenId, auction); 443 | } 444 | 445 | /// @dev Bids on an open auction, completing the auction and transferring 446 | /// ownership of the NFT if enough Ether is supplied. 447 | /// @param _tokenId - ID of token to bid on. 448 | function bid(uint256 _tokenId) 449 | external 450 | payable 451 | whenNotPaused 452 | { 453 | // _bid will throw if the bid or funds transfer fails 454 | _bid(_tokenId, msg.value); 455 | _transfer(msg.sender, _tokenId); 456 | } 457 | 458 | /// @dev Cancels an auction that hasn't been won yet. 459 | /// Returns the NFT to original owner. 460 | /// @notice This is a state-modifying function that can 461 | /// be called while the contract is paused. 462 | /// @param _tokenId - ID of token on auction 463 | function cancelAuction(uint256 _tokenId) 464 | external 465 | { 466 | Auction storage auction = tokenIdToAuction[_tokenId]; 467 | require(_isOnAuction(auction)); 468 | address seller = auction.seller; 469 | require(msg.sender == seller); 470 | _cancelAuction(_tokenId, seller); 471 | } 472 | 473 | /// @dev Cancels an auction when the contract is paused. 474 | /// Only the owner may do this, and NFTs are returned to 475 | /// the seller. This should only be used in emergencies. 476 | /// @param _tokenId - ID of the NFT on auction to cancel. 477 | function cancelAuctionWhenPaused(uint256 _tokenId) 478 | whenPaused 479 | onlyOwner 480 | external 481 | { 482 | Auction storage auction = tokenIdToAuction[_tokenId]; 483 | require(_isOnAuction(auction)); 484 | _cancelAuction(_tokenId, auction.seller); 485 | } 486 | 487 | /// @dev Returns auction info for an NFT on auction. 488 | /// @param _tokenId - ID of NFT on auction. 489 | function getAuction(uint256 _tokenId) 490 | external 491 | view 492 | returns 493 | ( 494 | address seller, 495 | uint256 startingPrice, 496 | uint256 endingPrice, 497 | uint256 duration, 498 | uint256 startedAt 499 | ) { 500 | Auction storage auction = tokenIdToAuction[_tokenId]; 501 | require(_isOnAuction(auction)); 502 | return ( 503 | auction.seller, 504 | auction.startingPrice, 505 | auction.endingPrice, 506 | auction.duration, 507 | auction.startedAt 508 | ); 509 | } 510 | 511 | /// @dev Returns the current price of an auction. 512 | /// @param _tokenId - ID of the token price we are checking. 513 | function getCurrentPrice(uint256 _tokenId) 514 | external 515 | view 516 | returns (uint256) 517 | { 518 | Auction storage auction = tokenIdToAuction[_tokenId]; 519 | require(_isOnAuction(auction)); 520 | return _currentPrice(auction); 521 | } 522 | 523 | } 524 | 525 | 526 | /// @title Reverse auction modified for siring 527 | /// @notice We omit a fallback function to prevent accidental sends to this contract. 528 | contract SiringClockAuction is ClockAuction { 529 | 530 | // @dev Sanity check that allows us to ensure that we are pointing to the 531 | // right auction in our setSiringAuctionAddress() call. 532 | bool public isSiringClockAuction = true; 533 | 534 | // Delegate constructor 535 | function SiringClockAuction(address _nftAddr, uint256 _cut) public 536 | ClockAuction(_nftAddr, _cut) {} 537 | 538 | /// @dev Creates and begins a new auction. Since this function is wrapped, 539 | /// require sender to be KittyCore contract. 540 | /// @param _tokenId - ID of token to auction, sender must be owner. 541 | /// @param _startingPrice - Price of item (in wei) at beginning of auction. 542 | /// @param _endingPrice - Price of item (in wei) at end of auction. 543 | /// @param _duration - Length of auction (in seconds). 544 | /// @param _seller - Seller, if not the message sender 545 | function createAuction( 546 | uint256 _tokenId, 547 | uint256 _startingPrice, 548 | uint256 _endingPrice, 549 | uint256 _duration, 550 | address _seller 551 | ) 552 | external 553 | { 554 | // Sanity check that no inputs overflow how many bits we've allocated 555 | // to store them in the auction struct. 556 | require(_startingPrice == uint256(uint128(_startingPrice))); 557 | require(_endingPrice == uint256(uint128(_endingPrice))); 558 | require(_duration == uint256(uint64(_duration))); 559 | 560 | require(msg.sender == address(nonFungibleContract)); 561 | _escrow(_seller, _tokenId); 562 | Auction memory auction = Auction( 563 | _seller, 564 | uint128(_startingPrice), 565 | uint128(_endingPrice), 566 | uint64(_duration), 567 | uint64(now) 568 | ); 569 | _addAuction(_tokenId, auction); 570 | } 571 | 572 | /// @dev Places a bid for siring. Requires the sender 573 | /// is the KittyCore contract because all bid methods 574 | /// should be wrapped. Also returns the kitty to the 575 | /// seller rather than the winner. 576 | function bid(uint256 _tokenId) 577 | external 578 | payable 579 | { 580 | require(msg.sender == address(nonFungibleContract)); 581 | address seller = tokenIdToAuction[_tokenId].seller; 582 | // _bid checks that token ID is valid and will throw if bid fails 583 | _bid(_tokenId, msg.value); 584 | // We transfer the kitty back to the seller, the winner will get 585 | // the offspring 586 | _transfer(seller, _tokenId); 587 | } 588 | 589 | } 590 | -------------------------------------------------------------------------------- /contracts/SaleClockAuction.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | 4 | /** 5 | * @title Ownable 6 | * @dev The Ownable contract has an owner address, and provides basic authorization control 7 | * functions, this simplifies the implementation of "user permissions". 8 | */ 9 | contract Ownable { 10 | address public owner; 11 | 12 | 13 | /** 14 | * @dev The Ownable constructor sets the original `owner` of the contract to the sender 15 | * account. 16 | */ 17 | function Ownable() { 18 | owner = msg.sender; 19 | } 20 | 21 | 22 | /** 23 | * @dev Throws if called by any account other than the owner. 24 | */ 25 | modifier onlyOwner() { 26 | require(msg.sender == owner); 27 | _; 28 | } 29 | 30 | 31 | /** 32 | * @dev Allows the current owner to transfer control of the contract to a newOwner. 33 | * @param newOwner The address to transfer ownership to. 34 | */ 35 | function transferOwnership(address newOwner) onlyOwner { 36 | if (newOwner != address(0)) { 37 | owner = newOwner; 38 | } 39 | } 40 | 41 | } 42 | 43 | 44 | 45 | /// @title Interface for contracts conforming to ERC-721: Non-Fungible Tokens 46 | /// @author Dieter Shirley (https://github.com/dete) 47 | contract ERC721 { 48 | // Required methods 49 | function totalSupply() public view returns (uint256 total); 50 | function balanceOf(address _owner) public view returns (uint256 balance); 51 | function ownerOf(uint256 _tokenId) external view returns (address owner); 52 | function approve(address _to, uint256 _tokenId) external; 53 | function transfer(address _to, uint256 _tokenId) external; 54 | function transferFrom(address _from, address _to, uint256 _tokenId) external; 55 | 56 | // Events 57 | event Transfer(address from, address to, uint256 tokenId); 58 | event Approval(address owner, address approved, uint256 tokenId); 59 | 60 | // Optional 61 | // function name() public view returns (string name); 62 | // function symbol() public view returns (string symbol); 63 | // function tokensOfOwner(address _owner) external view returns (uint256[] tokenIds); 64 | // function tokenMetadata(uint256 _tokenId, string _preferredTransport) public view returns (string infoUrl); 65 | 66 | // ERC-165 Compatibility (https://github.com/ethereum/EIPs/issues/165) 67 | function supportsInterface(bytes4 _interfaceID) external view returns (bool); 68 | } 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | /// @title Auction Core 79 | /// @dev Contains models, variables, and internal methods for the auction. 80 | /// @notice We omit a fallback function to prevent accidental sends to this contract. 81 | contract ClockAuctionBase { 82 | 83 | // Represents an auction on an NFT 84 | struct Auction { 85 | // Current owner of NFT 86 | address seller; 87 | // Price (in wei) at beginning of auction 88 | uint128 startingPrice; 89 | // Price (in wei) at end of auction 90 | uint128 endingPrice; 91 | // Duration (in seconds) of auction 92 | uint64 duration; 93 | // Time when auction started 94 | // NOTE: 0 if this auction has been concluded 95 | uint64 startedAt; 96 | } 97 | 98 | // Reference to contract tracking NFT ownership 99 | ERC721 public nonFungibleContract; 100 | 101 | // Cut owner takes on each auction, measured in basis points (1/100 of a percent). 102 | // Values 0-10,000 map to 0%-100% 103 | uint256 public ownerCut; 104 | 105 | // Map from token ID to their corresponding auction. 106 | mapping (uint256 => Auction) tokenIdToAuction; 107 | 108 | event AuctionCreated(uint256 tokenId, uint256 startingPrice, uint256 endingPrice, uint256 duration); 109 | event AuctionSuccessful(uint256 tokenId, uint256 totalPrice, address winner); 110 | event AuctionCancelled(uint256 tokenId); 111 | 112 | /// @dev Returns true if the claimant owns the token. 113 | /// @param _claimant - Address claiming to own the token. 114 | /// @param _tokenId - ID of token whose ownership to verify. 115 | function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) { 116 | return (nonFungibleContract.ownerOf(_tokenId) == _claimant); 117 | } 118 | 119 | /// @dev Escrows the NFT, assigning ownership to this contract. 120 | /// Throws if the escrow fails. 121 | /// @param _owner - Current owner address of token to escrow. 122 | /// @param _tokenId - ID of token whose approval to verify. 123 | function _escrow(address _owner, uint256 _tokenId) internal { 124 | // it will throw if transfer fails 125 | nonFungibleContract.transferFrom(_owner, this, _tokenId); 126 | } 127 | 128 | /// @dev Transfers an NFT owned by this contract to another address. 129 | /// Returns true if the transfer succeeds. 130 | /// @param _receiver - Address to transfer NFT to. 131 | /// @param _tokenId - ID of token to transfer. 132 | function _transfer(address _receiver, uint256 _tokenId) internal { 133 | // it will throw if transfer fails 134 | nonFungibleContract.transfer(_receiver, _tokenId); 135 | } 136 | 137 | /// @dev Adds an auction to the list of open auctions. Also fires the 138 | /// AuctionCreated event. 139 | /// @param _tokenId The ID of the token to be put on auction. 140 | /// @param _auction Auction to add. 141 | function _addAuction(uint256 _tokenId, Auction _auction) internal { 142 | // Require that all auctions have a duration of 143 | // at least one minute. (Keeps our math from getting hairy!) 144 | require(_auction.duration >= 1 minutes); 145 | 146 | tokenIdToAuction[_tokenId] = _auction; 147 | 148 | AuctionCreated( 149 | uint256(_tokenId), 150 | uint256(_auction.startingPrice), 151 | uint256(_auction.endingPrice), 152 | uint256(_auction.duration) 153 | ); 154 | } 155 | 156 | /// @dev Cancels an auction unconditionally. 157 | function _cancelAuction(uint256 _tokenId, address _seller) internal { 158 | _removeAuction(_tokenId); 159 | _transfer(_seller, _tokenId); 160 | AuctionCancelled(_tokenId); 161 | } 162 | 163 | /// @dev Computes the price and transfers winnings. 164 | /// Does NOT transfer ownership of token. 165 | function _bid(uint256 _tokenId, uint256 _bidAmount) 166 | internal 167 | returns (uint256) 168 | { 169 | // Get a reference to the auction struct 170 | Auction storage auction = tokenIdToAuction[_tokenId]; 171 | 172 | // Explicitly check that this auction is currently live. 173 | // (Because of how Ethereum mappings work, we can't just count 174 | // on the lookup above failing. An invalid _tokenId will just 175 | // return an auction object that is all zeros.) 176 | require(_isOnAuction(auction)); 177 | 178 | // Check that the bid is greater than or equal to the current price 179 | uint256 price = _currentPrice(auction); 180 | require(_bidAmount >= price); 181 | 182 | // Grab a reference to the seller before the auction struct 183 | // gets deleted. 184 | address seller = auction.seller; 185 | 186 | // The bid is good! Remove the auction before sending the fees 187 | // to the sender so we can't have a reentrancy attack. 188 | _removeAuction(_tokenId); 189 | 190 | // Transfer proceeds to seller (if there are any!) 191 | if (price > 0) { 192 | // Calculate the auctioneer's cut. 193 | // (NOTE: _computeCut() is guaranteed to return a 194 | // value <= price, so this subtraction can't go negative.) 195 | uint256 auctioneerCut = _computeCut(price); 196 | uint256 sellerProceeds = price - auctioneerCut; 197 | 198 | // NOTE: Doing a transfer() in the middle of a complex 199 | // method like this is generally discouraged because of 200 | // reentrancy attacks and DoS attacks if the seller is 201 | // a contract with an invalid fallback function. We explicitly 202 | // guard against reentrancy attacks by removing the auction 203 | // before calling transfer(), and the only thing the seller 204 | // can DoS is the sale of their own asset! (And if it's an 205 | // accident, they can call cancelAuction(). ) 206 | seller.transfer(sellerProceeds); 207 | } 208 | 209 | // Calculate any excess funds included with the bid. If the excess 210 | // is anything worth worrying about, transfer it back to bidder. 211 | // NOTE: We checked above that the bid amount is greater than or 212 | // equal to the price so this cannot underflow. 213 | uint256 bidExcess = _bidAmount - price; 214 | 215 | // Return the funds. Similar to the previous transfer, this is 216 | // not susceptible to a re-entry attack because the auction is 217 | // removed before any transfers occur. 218 | msg.sender.transfer(bidExcess); 219 | 220 | // Tell the world! 221 | AuctionSuccessful(_tokenId, price, msg.sender); 222 | 223 | return price; 224 | } 225 | 226 | /// @dev Removes an auction from the list of open auctions. 227 | /// @param _tokenId - ID of NFT on auction. 228 | function _removeAuction(uint256 _tokenId) internal { 229 | delete tokenIdToAuction[_tokenId]; 230 | } 231 | 232 | /// @dev Returns true if the NFT is on auction. 233 | /// @param _auction - Auction to check. 234 | function _isOnAuction(Auction storage _auction) internal view returns (bool) { 235 | return (_auction.startedAt > 0); 236 | } 237 | 238 | /// @dev Returns current price of an NFT on auction. Broken into two 239 | /// functions (this one, that computes the duration from the auction 240 | /// structure, and the other that does the price computation) so we 241 | /// can easily test that the price computation works correctly. 242 | function _currentPrice(Auction storage _auction) 243 | internal 244 | view 245 | returns (uint256) 246 | { 247 | uint256 secondsPassed = 0; 248 | 249 | // A bit of insurance against negative values (or wraparound). 250 | // Probably not necessary (since Ethereum guarnatees that the 251 | // now variable doesn't ever go backwards). 252 | if (now > _auction.startedAt) { 253 | secondsPassed = now - _auction.startedAt; 254 | } 255 | 256 | return _computeCurrentPrice( 257 | _auction.startingPrice, 258 | _auction.endingPrice, 259 | _auction.duration, 260 | secondsPassed 261 | ); 262 | } 263 | 264 | /// @dev Computes the current price of an auction. Factored out 265 | /// from _currentPrice so we can run extensive unit tests. 266 | /// When testing, make this function public and turn on 267 | /// `Current price computation` test suite. 268 | function _computeCurrentPrice( 269 | uint256 _startingPrice, 270 | uint256 _endingPrice, 271 | uint256 _duration, 272 | uint256 _secondsPassed 273 | ) 274 | internal 275 | pure 276 | returns (uint256) 277 | { 278 | // NOTE: We don't use SafeMath (or similar) in this function because 279 | // all of our public functions carefully cap the maximum values for 280 | // time (at 64-bits) and currency (at 128-bits). _duration is 281 | // also known to be non-zero (see the require() statement in 282 | // _addAuction()) 283 | if (_secondsPassed >= _duration) { 284 | // We've reached the end of the dynamic pricing portion 285 | // of the auction, just return the end price. 286 | return _endingPrice; 287 | } else { 288 | // Starting price can be higher than ending price (and often is!), so 289 | // this delta can be negative. 290 | int256 totalPriceChange = int256(_endingPrice) - int256(_startingPrice); 291 | 292 | // This multiplication can't overflow, _secondsPassed will easily fit within 293 | // 64-bits, and totalPriceChange will easily fit within 128-bits, their product 294 | // will always fit within 256-bits. 295 | int256 currentPriceChange = totalPriceChange * int256(_secondsPassed) / int256(_duration); 296 | 297 | // currentPriceChange can be negative, but if so, will have a magnitude 298 | // less that _startingPrice. Thus, this result will always end up positive. 299 | int256 currentPrice = int256(_startingPrice) + currentPriceChange; 300 | 301 | return uint256(currentPrice); 302 | } 303 | } 304 | 305 | /// @dev Computes owner's cut of a sale. 306 | /// @param _price - Sale price of NFT. 307 | function _computeCut(uint256 _price) internal view returns (uint256) { 308 | // NOTE: We don't use SafeMath (or similar) in this function because 309 | // all of our entry functions carefully cap the maximum values for 310 | // currency (at 128-bits), and ownerCut <= 10000 (see the require() 311 | // statement in the ClockAuction constructor). The result of this 312 | // function is always guaranteed to be <= _price. 313 | return _price * ownerCut / 10000; 314 | } 315 | 316 | } 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | /** 325 | * @title Pausable 326 | * @dev Base contract which allows children to implement an emergency stop mechanism. 327 | */ 328 | contract Pausable is Ownable { 329 | event Pause(); 330 | event Unpause(); 331 | 332 | bool public paused = false; 333 | 334 | 335 | /** 336 | * @dev modifier to allow actions only when the contract IS paused 337 | */ 338 | modifier whenNotPaused() { 339 | require(!paused); 340 | _; 341 | } 342 | 343 | /** 344 | * @dev modifier to allow actions only when the contract IS NOT paused 345 | */ 346 | modifier whenPaused { 347 | require(paused); 348 | _; 349 | } 350 | 351 | /** 352 | * @dev called by the owner to pause, triggers stopped state 353 | */ 354 | function pause() onlyOwner whenNotPaused returns (bool) { 355 | paused = true; 356 | Pause(); 357 | return true; 358 | } 359 | 360 | /** 361 | * @dev called by the owner to unpause, returns to normal state 362 | */ 363 | function unpause() onlyOwner whenPaused returns (bool) { 364 | paused = false; 365 | Unpause(); 366 | return true; 367 | } 368 | } 369 | 370 | 371 | /// @title Clock auction for non-fungible tokens. 372 | /// @notice We omit a fallback function to prevent accidental sends to this contract. 373 | contract ClockAuction is Pausable, ClockAuctionBase { 374 | 375 | /// @dev The ERC-165 interface signature for ERC-721. 376 | /// Ref: https://github.com/ethereum/EIPs/issues/165 377 | /// Ref: https://github.com/ethereum/EIPs/issues/721 378 | bytes4 constant InterfaceSignature_ERC721 = bytes4(0x9a20483d); 379 | 380 | /// @dev Constructor creates a reference to the NFT ownership contract 381 | /// and verifies the owner cut is in the valid range. 382 | /// @param _nftAddress - address of a deployed contract implementing 383 | /// the Nonfungible Interface. 384 | /// @param _cut - percent cut the owner takes on each auction, must be 385 | /// between 0-10,000. 386 | function ClockAuction(address _nftAddress, uint256 _cut) public { 387 | require(_cut <= 10000); 388 | ownerCut = _cut; 389 | 390 | ERC721 candidateContract = ERC721(_nftAddress); 391 | require(candidateContract.supportsInterface(InterfaceSignature_ERC721)); 392 | nonFungibleContract = candidateContract; 393 | } 394 | 395 | /// @dev Remove all Ether from the contract, which is the owner's cuts 396 | /// as well as any Ether sent directly to the contract address. 397 | /// Always transfers to the NFT contract, but can be called either by 398 | /// the owner or the NFT contract. 399 | function withdrawBalance() external { 400 | address nftAddress = address(nonFungibleContract); 401 | 402 | require( 403 | msg.sender == owner || 404 | msg.sender == nftAddress 405 | ); 406 | // We are using this boolean method to make sure that even if one fails it will still work 407 | bool res = nftAddress.send(this.balance); 408 | } 409 | 410 | /// @dev Creates and begins a new auction. 411 | /// @param _tokenId - ID of token to auction, sender must be owner. 412 | /// @param _startingPrice - Price of item (in wei) at beginning of auction. 413 | /// @param _endingPrice - Price of item (in wei) at end of auction. 414 | /// @param _duration - Length of time to move between starting 415 | /// price and ending price (in seconds). 416 | /// @param _seller - Seller, if not the message sender 417 | function createAuction( 418 | uint256 _tokenId, 419 | uint256 _startingPrice, 420 | uint256 _endingPrice, 421 | uint256 _duration, 422 | address _seller 423 | ) 424 | external 425 | whenNotPaused 426 | { 427 | // Sanity check that no inputs overflow how many bits we've allocated 428 | // to store them in the auction struct. 429 | require(_startingPrice == uint256(uint128(_startingPrice))); 430 | require(_endingPrice == uint256(uint128(_endingPrice))); 431 | require(_duration == uint256(uint64(_duration))); 432 | 433 | require(_owns(msg.sender, _tokenId)); 434 | _escrow(msg.sender, _tokenId); 435 | Auction memory auction = Auction( 436 | _seller, 437 | uint128(_startingPrice), 438 | uint128(_endingPrice), 439 | uint64(_duration), 440 | uint64(now) 441 | ); 442 | _addAuction(_tokenId, auction); 443 | } 444 | 445 | /// @dev Bids on an open auction, completing the auction and transferring 446 | /// ownership of the NFT if enough Ether is supplied. 447 | /// @param _tokenId - ID of token to bid on. 448 | function bid(uint256 _tokenId) 449 | external 450 | payable 451 | whenNotPaused 452 | { 453 | // _bid will throw if the bid or funds transfer fails 454 | _bid(_tokenId, msg.value); 455 | _transfer(msg.sender, _tokenId); 456 | } 457 | 458 | /// @dev Cancels an auction that hasn't been won yet. 459 | /// Returns the NFT to original owner. 460 | /// @notice This is a state-modifying function that can 461 | /// be called while the contract is paused. 462 | /// @param _tokenId - ID of token on auction 463 | function cancelAuction(uint256 _tokenId) 464 | external 465 | { 466 | Auction storage auction = tokenIdToAuction[_tokenId]; 467 | require(_isOnAuction(auction)); 468 | address seller = auction.seller; 469 | require(msg.sender == seller); 470 | _cancelAuction(_tokenId, seller); 471 | } 472 | 473 | /// @dev Cancels an auction when the contract is paused. 474 | /// Only the owner may do this, and NFTs are returned to 475 | /// the seller. This should only be used in emergencies. 476 | /// @param _tokenId - ID of the NFT on auction to cancel. 477 | function cancelAuctionWhenPaused(uint256 _tokenId) 478 | whenPaused 479 | onlyOwner 480 | external 481 | { 482 | Auction storage auction = tokenIdToAuction[_tokenId]; 483 | require(_isOnAuction(auction)); 484 | _cancelAuction(_tokenId, auction.seller); 485 | } 486 | 487 | /// @dev Returns auction info for an NFT on auction. 488 | /// @param _tokenId - ID of NFT on auction. 489 | function getAuction(uint256 _tokenId) 490 | external 491 | view 492 | returns 493 | ( 494 | address seller, 495 | uint256 startingPrice, 496 | uint256 endingPrice, 497 | uint256 duration, 498 | uint256 startedAt 499 | ) { 500 | Auction storage auction = tokenIdToAuction[_tokenId]; 501 | require(_isOnAuction(auction)); 502 | return ( 503 | auction.seller, 504 | auction.startingPrice, 505 | auction.endingPrice, 506 | auction.duration, 507 | auction.startedAt 508 | ); 509 | } 510 | 511 | /// @dev Returns the current price of an auction. 512 | /// @param _tokenId - ID of the token price we are checking. 513 | function getCurrentPrice(uint256 _tokenId) 514 | external 515 | view 516 | returns (uint256) 517 | { 518 | Auction storage auction = tokenIdToAuction[_tokenId]; 519 | require(_isOnAuction(auction)); 520 | return _currentPrice(auction); 521 | } 522 | 523 | } 524 | 525 | 526 | /// @title Clock auction modified for sale of kitties 527 | /// @notice We omit a fallback function to prevent accidental sends to this contract. 528 | contract SaleClockAuction is ClockAuction { 529 | 530 | // @dev Sanity check that allows us to ensure that we are pointing to the 531 | // right auction in our setSaleAuctionAddress() call. 532 | bool public isSaleClockAuction = true; 533 | 534 | // Tracks last 5 sale price of gen0 kitty sales 535 | uint256 public gen0SaleCount; 536 | uint256[5] public lastGen0SalePrices; 537 | 538 | // Delegate constructor 539 | function SaleClockAuction(address _nftAddr, uint256 _cut) public 540 | ClockAuction(_nftAddr, _cut) {} 541 | 542 | /// @dev Creates and begins a new auction. 543 | /// @param _tokenId - ID of token to auction, sender must be owner. 544 | /// @param _startingPrice - Price of item (in wei) at beginning of auction. 545 | /// @param _endingPrice - Price of item (in wei) at end of auction. 546 | /// @param _duration - Length of auction (in seconds). 547 | /// @param _seller - Seller, if not the message sender 548 | function createAuction( 549 | uint256 _tokenId, 550 | uint256 _startingPrice, 551 | uint256 _endingPrice, 552 | uint256 _duration, 553 | address _seller 554 | ) 555 | external 556 | { 557 | // Sanity check that no inputs overflow how many bits we've allocated 558 | // to store them in the auction struct. 559 | require(_startingPrice == uint256(uint128(_startingPrice))); 560 | require(_endingPrice == uint256(uint128(_endingPrice))); 561 | require(_duration == uint256(uint64(_duration))); 562 | 563 | require(msg.sender == address(nonFungibleContract)); 564 | _escrow(_seller, _tokenId); 565 | Auction memory auction = Auction( 566 | _seller, 567 | uint128(_startingPrice), 568 | uint128(_endingPrice), 569 | uint64(_duration), 570 | uint64(now) 571 | ); 572 | _addAuction(_tokenId, auction); 573 | } 574 | 575 | /// @dev Updates lastSalePrice if seller is the nft contract 576 | /// Otherwise, works the same as default bid method. 577 | function bid(uint256 _tokenId) 578 | external 579 | payable 580 | { 581 | // _bid verifies token ID size 582 | address seller = tokenIdToAuction[_tokenId].seller; 583 | uint256 price = _bid(_tokenId, msg.value); 584 | _transfer(msg.sender, _tokenId); 585 | 586 | // If not a gen0 auction, exit 587 | if (seller == address(nonFungibleContract)) { 588 | // Track gen0 sale prices 589 | lastGen0SalePrices[gen0SaleCount % 5] = price; 590 | gen0SaleCount++; 591 | } 592 | } 593 | 594 | function averageGen0SalePrice() external view returns (uint256) { 595 | uint256 sum = 0; 596 | for (uint256 i = 0; i < 5; i++) { 597 | sum += lastGen0SalePrices[i]; 598 | } 599 | return sum / 5; 600 | } 601 | 602 | } 603 | -------------------------------------------------------------------------------- /api/static/vue.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Vue.js v2.5.16 3 | * (c) 2014-2018 Evan You 4 | * Released under the MIT License. 5 | */ 6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";var y=Object.freeze({});function M(e){return null==e}function D(e){return null!=e}function S(e){return!0===e}function T(e){return"string"==typeof e||"number"==typeof e||"symbol"==typeof e||"boolean"==typeof e}function P(e){return null!==e&&"object"==typeof e}var r=Object.prototype.toString;function l(e){return"[object Object]"===r.call(e)}function i(e){var t=parseFloat(String(e));return 0<=t&&Math.floor(t)===t&&isFinite(e)}function t(e){return null==e?"":"object"==typeof e?JSON.stringify(e,null,2):String(e)}function F(e){var t=parseFloat(e);return isNaN(t)?e:t}function s(e,t){for(var n=Object.create(null),r=e.split(","),i=0;ie.id;)n--;bt.splice(n+1,0,e)}else bt.push(e);Ct||(Ct=!0,Ze(At))}}(this)},St.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||P(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){Fe(e,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,e,t)}}},St.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},St.prototype.depend=function(){for(var e=this.deps.length;e--;)this.deps[e].depend()},St.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||f(this.vm._watchers,this);for(var e=this.deps.length;e--;)this.deps[e].removeSub(this);this.active=!1}};var Tt={enumerable:!0,configurable:!0,get:$,set:$};function Et(e,t,n){Tt.get=function(){return this[t][n]},Tt.set=function(e){this[t][n]=e},Object.defineProperty(e,n,Tt)}function jt(e){e._watchers=[];var t=e.$options;t.props&&function(n,r){var i=n.$options.propsData||{},o=n._props={},a=n.$options._propKeys=[];n.$parent&&ge(!1);var e=function(e){a.push(e);var t=Ie(e,r,i,n);Ce(o,e,t),e in n||Et(n,"_props",e)};for(var t in r)e(t);ge(!0)}(e,t.props),t.methods&&function(e,t){e.$options.props;for(var n in t)e[n]=null==t[n]?$:v(t[n],e)}(e,t.methods),t.data?function(e){var t=e.$options.data;l(t=e._data="function"==typeof t?function(e,t){se();try{return e.call(t,t)}catch(e){return Fe(e,t,"data()"),{}}finally{ce()}}(t,e):t||{})||(t={});var n=Object.keys(t),r=e.$options.props,i=(e.$options.methods,n.length);for(;i--;){var o=n[i];r&&p(r,o)||(void 0,36!==(a=(o+"").charCodeAt(0))&&95!==a&&Et(e,"_data",o))}var a;we(t,!0)}(e):we(e._data={},!0),t.computed&&function(e,t){var n=e._computedWatchers=Object.create(null),r=Y();for(var i in t){var o=t[i],a="function"==typeof o?o:o.get;r||(n[i]=new St(e,a||$,$,Nt)),i in e||Lt(e,i,o)}}(e,t.computed),t.watch&&t.watch!==G&&function(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;iparseInt(this.max)&&bn(a,s[0],s,this._vnode)),t.data.keepAlive=!0}return t||e&&e[0]}}};$n=hn,Cn={get:function(){return j}},Object.defineProperty($n,"config",Cn),$n.util={warn:re,extend:m,mergeOptions:Ne,defineReactive:Ce},$n.set=xe,$n.delete=ke,$n.nextTick=Ze,$n.options=Object.create(null),k.forEach(function(e){$n.options[e+"s"]=Object.create(null)}),m(($n.options._base=$n).options.components,kn),$n.use=function(e){var t=this._installedPlugins||(this._installedPlugins=[]);if(-1=a&&l()};setTimeout(function(){c\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,oo="[a-zA-Z_][\\w\\-\\.]*",ao="((?:"+oo+"\\:)?"+oo+")",so=new RegExp("^<"+ao),co=/^\s*(\/?)>/,lo=new RegExp("^<\\/"+ao+"[^>]*>"),uo=/^]+>/i,fo=/^",""":'"',"&":"&"," ":"\n"," ":"\t"},go=/&(?:lt|gt|quot|amp);/g,_o=/&(?:lt|gt|quot|amp|#10|#9);/g,bo=s("pre,textarea",!0),$o=function(e,t){return e&&bo(e)&&"\n"===t[0]};var wo,Co,xo,ko,Ao,Oo,So,To,Eo=/^@|^v-on:/,jo=/^v-|^@|^:/,No=/([^]*?)\s+(?:in|of)\s+([^]*)/,Lo=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,Io=/^\(|\)$/g,Mo=/:(.*)$/,Do=/^:|^v-bind:/,Po=/\.[^.]+/g,Fo=e(eo);function Ro(e,t,n){return{type:1,tag:e,attrsList:t,attrsMap:function(e){for(var t={},n=0,r=e.length;n]*>)","i")),n=i.replace(t,function(e,t,n){return r=n.length,ho(o)||"noscript"===o||(t=t.replace(//g,"$1").replace(//g,"$1")),$o(o,t)&&(t=t.slice(1)),d.chars&&d.chars(t),""});a+=i.length-n.length,i=n,A(o,a-r,a)}else{var s=i.indexOf("<");if(0===s){if(fo.test(i)){var c=i.indexOf("--\x3e");if(0<=c){d.shouldKeepComment&&d.comment(i.substring(4,c)),C(c+3);continue}}if(po.test(i)){var l=i.indexOf("]>");if(0<=l){C(l+2);continue}}var u=i.match(uo);if(u){C(u[0].length);continue}var f=i.match(lo);if(f){var p=a;C(f[0].length),A(f[1],p,a);continue}var _=x();if(_){k(_),$o(v,i)&&C(1);continue}}var b=void 0,$=void 0,w=void 0;if(0<=s){for($=i.slice(s);!(lo.test($)||so.test($)||fo.test($)||po.test($)||(w=$.indexOf("<",1))<0);)s+=w,$=i.slice(s);b=i.substring(0,s),C(s)}s<0&&(b=i,i=""),d.chars&&b&&d.chars(b)}if(i===e){d.chars&&d.chars(i);break}}function C(e){a+=e,i=i.substring(e)}function x(){var e=i.match(so);if(e){var t,n,r={tagName:e[1],attrs:[],start:a};for(C(e[0].length);!(t=i.match(co))&&(n=i.match(io));)C(n[0].length),r.attrs.push(n);if(t)return r.unarySlash=t[1],C(t[0].length),r.end=a,r}}function k(e){var t=e.tagName,n=e.unarySlash;m&&("p"===v&&ro(t)&&A(v),g(t)&&v===t&&A(t));for(var r,i,o,a=y(t)||!!n,s=e.attrs.length,c=new Array(s),l=0;l-1"+("true"===d?":("+l+")":":_q("+l+","+d+")")),Ar(c,"change","var $$a="+l+",$$el=$event.target,$$c=$$el.checked?("+d+"):("+v+");if(Array.isArray($$a)){var $$v="+(f?"_n("+p+")":p)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+Er(l,"$$a.concat([$$v])")+")}else{$$i>-1&&("+Er(l,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+Er(l,"$$c")+"}",null,!0);else if("input"===$&&"radio"===w)r=e,i=_,a=(o=b)&&o.number,s=Or(r,"value")||"null",Cr(r,"checked","_q("+i+","+(s=a?"_n("+s+")":s)+")"),Ar(r,"change",Er(i,s),null,!0);else if("input"===$||"textarea"===$)!function(e,t,n){var r=e.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,l=o?"change":"range"===r?Pr:"input",u="$event.target.value";s&&(u="$event.target.value.trim()"),a&&(u="_n("+u+")");var f=Er(t,u);c&&(f="if($event.target.composing)return;"+f),Cr(e,"value","("+t+")"),Ar(e,l,f,null,!0),(s||a)&&Ar(e,"blur","$forceUpdate()")}(e,_,b);else if(!j.isReservedTag($))return Tr(e,_,b),!1;return!0},text:function(e,t){t.value&&Cr(e,"textContent","_s("+t.value+")")},html:function(e,t){t.value&&Cr(e,"innerHTML","_s("+t.value+")")}},isPreTag:function(e){return"pre"===e},isUnaryTag:to,mustUseProp:Sn,canBeLeftOpenTag:no,isReservedTag:Un,getTagNamespace:Vn,staticKeys:(Go=Wo,Go.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(","))},Qo=e(function(e){return s("type,tag,attrsList,attrsMap,plain,parent,children,attrs"+(e?","+e:""))});function ea(e,t){e&&(Zo=Qo(t.staticKeys||""),Xo=t.isReservedTag||O,function e(t){t.static=function(e){if(2===e.type)return!1;if(3===e.type)return!0;return!(!e.pre&&(e.hasBindings||e.if||e.for||c(e.tag)||!Xo(e.tag)||function(e){for(;e.parent;){if("template"!==(e=e.parent).tag)return!1;if(e.for)return!0}return!1}(e)||!Object.keys(e).every(Zo)))}(t);if(1===t.type){if(!Xo(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var n=0,r=t.children.length;n|^function\s*\(/,na=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,ra={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},ia={esc:"Escape",tab:"Tab",enter:"Enter",space:" ",up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete"]},oa=function(e){return"if("+e+")return null;"},aa={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:oa("$event.target !== $event.currentTarget"),ctrl:oa("!$event.ctrlKey"),shift:oa("!$event.shiftKey"),alt:oa("!$event.altKey"),meta:oa("!$event.metaKey"),left:oa("'button' in $event && $event.button !== 0"),middle:oa("'button' in $event && $event.button !== 1"),right:oa("'button' in $event && $event.button !== 2")};function sa(e,t,n){var r=t?"nativeOn:{":"on:{";for(var i in e)r+='"'+i+'":'+ca(i,e[i])+",";return r.slice(0,-1)+"}"}function ca(t,e){if(!e)return"function(){}";if(Array.isArray(e))return"["+e.map(function(e){return ca(t,e)}).join(",")+"]";var n=na.test(e.value),r=ta.test(e.value);if(e.modifiers){var i="",o="",a=[];for(var s in e.modifiers)if(aa[s])o+=aa[s],ra[s]&&a.push(s);else if("exact"===s){var c=e.modifiers;o+=oa(["ctrl","shift","alt","meta"].filter(function(e){return!c[e]}).map(function(e){return"$event."+e+"Key"}).join("||"))}else a.push(s);return a.length&&(i+="if(!('button' in $event)&&"+a.map(la).join("&&")+")return null;"),o&&(i+=o),"function($event){"+i+(n?"return "+e.value+"($event)":r?"return ("+e.value+")($event)":e.value)+"}"}return n||r?e.value:"function($event){"+e.value+"}"}function la(e){var t=parseInt(e,10);if(t)return"$event.keyCode!=="+t;var n=ra[e],r=ia[e];return"_k($event.keyCode,"+JSON.stringify(e)+","+JSON.stringify(n)+",$event.key,"+JSON.stringify(r)+")"}var ua={on:function(e,t){e.wrapListeners=function(e){return"_g("+e+","+t.value+")"}},bind:function(t,n){t.wrapData=function(e){return"_b("+e+",'"+t.tag+"',"+n.value+","+(n.modifiers&&n.modifiers.prop?"true":"false")+(n.modifiers&&n.modifiers.sync?",true":"")+")"}},cloak:$},fa=function(e){this.options=e,this.warn=e.warn||$r,this.transforms=wr(e.modules,"transformCode"),this.dataGenFns=wr(e.modules,"genData"),this.directives=m(m({},ua),e.directives);var t=e.isReservedTag||O;this.maybeComponent=function(e){return!t(e.tag)},this.onceId=0,this.staticRenderFns=[]};function pa(e,t){var n=new fa(t);return{render:"with(this){return "+(e?da(e,n):'_c("div")')+"}",staticRenderFns:n.staticRenderFns}}function da(e,t){if(e.staticRoot&&!e.staticProcessed)return va(e,t);if(e.once&&!e.onceProcessed)return ha(e,t);if(e.for&&!e.forProcessed)return f=t,v=(u=e).for,h=u.alias,m=u.iterator1?","+u.iterator1:"",y=u.iterator2?","+u.iterator2:"",u.forProcessed=!0,(d||"_l")+"(("+v+"),function("+h+m+y+"){return "+(p||da)(u,f)+"})";if(e.if&&!e.ifProcessed)return ma(e,t);if("template"!==e.tag||e.slotTarget){if("slot"===e.tag)return function(e,t){var n=e.slotName||'"default"',r=_a(e,t),i="_t("+n+(r?","+r:""),o=e.attrs&&"{"+e.attrs.map(function(e){return g(e.name)+":"+e.value}).join(",")+"}",a=e.attrsMap["v-bind"];!o&&!a||r||(i+=",null");o&&(i+=","+o);a&&(i+=(o?"":",null")+","+a);return i+")"}(e,t);var n;if(e.component)a=e.component,c=t,l=(s=e).inlineTemplate?null:_a(s,c,!0),n="_c("+a+","+ya(s,c)+(l?","+l:"")+")";else{var r=e.plain?void 0:ya(e,t),i=e.inlineTemplate?null:_a(e,t,!0);n="_c('"+e.tag+"'"+(r?","+r:"")+(i?","+i:"")+")"}for(var o=0;o':'
',0