ETH Price: $1,961.92 (-1.57%)

Transaction Decoder

Block:
8822956 at Oct-27-2019 05:39:25 PM +UTC
Transaction Fee:
0.000316222 ETH $0.62
Gas Used:
158,111 Gas / 2 Gwei

Emitted Events:

154 Chest.Transfer( from=[Sender] 0x0eb9a7ff5cbf719251989caf1599c1270eafb531, to=0x0000000000000000000000000000000000000000, value=1 )
155 PackFive.PurchaseRecorded( id=2061, packType=0, user=[Sender] 0x0eb9a7ff5cbf719251989caf1599c1270eafb531, count=6, lockup=0 )
156 PackFive.ChestsOpened( id=2061, packType=0, user=[Sender] 0x0eb9a7ff5cbf719251989caf1599c1270eafb531, count=1, packCount=6 )

Account State Difference:

  Address   Before After State Difference Code
(zhizhu.top)
1,272.959349781333901974 Eth1,272.959666003333901974 Eth0.000316222
0x0eb9a7ff...70Eafb531
1.92216355519965201 Eth
Nonce: 249
1.92184733319965201 Eth
Nonce: 250
0.000316222
0x3aE323c0...900d8B50D
0xEE85966b...3aFbc2B68

Execution Trace

Chest.open( value=1 ) => ( 2061 )
  • PackFive.openChest( packType=0, user=0x0eb9a7ff5cBf719251989CAf1599c1270Eafb531, count=1 ) => ( 2061 )
    File 1 of 2: Chest
    pragma solidity ^0.5.0;
    
    // from OZ
    
    /**
     * @title SafeMath
     * @dev Math operations with safety checks that throw on error
     */
    library SafeMath {
    
        /**
        * @dev Multiplies two numbers, throws on overflow.
        */
        function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {
            // Gas optimization: this is cheaper than asserting 'a' not being zero, but the
            // benefit is lost if 'b' is also tested.
            // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
            if (a == 0) {
                return 0;
            }
    
            c = a * b;
            assert(c / a == b);
            return c;
        }
    
        /**
        * @dev Integer division of two numbers, truncating the quotient.
        */
        function div(uint256 a, uint256 b) internal pure returns (uint256) {
            // assert(b > 0); // Solidity automatically throws when dividing by 0
            // uint256 c = a / b;
            // assert(a == b * c + a % b); // There is no case in which this doesn't hold
            return a / b;
        }
    
        /**
        * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
        */
        function sub(uint256 a, uint256 b) internal pure returns (uint256) {
            assert(b <= a);
            return a - b;
        }
    
        /**
        * @dev Adds two numbers, throws on overflow.
        */
        function add(uint256 a, uint256 b) internal pure returns (uint256 c) {
            c = a + b;
            assert(c >= a);
            return c;
        }
    }
    
    interface IProcessor {
    
        function processPayment(address user, uint cost, uint items, address referrer) external payable returns (uint id);
        
    }
    
    contract Pack {
    
        enum Type {
            Rare, Epic, Legendary, Shiny
        }
    
    }
    
    contract Ownable {
    
        address payable public owner;
    
        constructor() public {
            owner = msg.sender;
        }
    
        function setOwner(address payable _owner) public onlyOwner {
            owner = _owner;
        }
    
        function getOwner() public view returns (address payable) {
            return owner;
        }
    
        modifier onlyOwner {
            require(msg.sender == owner, "must be owner to call this function");
            _;
        }
    
    }
    
    contract IERC20 {
    
        event Approval(address indexed owner, address indexed spender, uint256 value);
        event Transfer(address indexed from, address indexed to, uint256 value);
        
        function allowance(address owner, address spender) public view returns (uint256);
        
        function transferFrom(address from, address to, uint256 value) public returns (bool);
    
        function approve(address spender, uint256 value) public returns (bool);
    
        function totalSupply() public view returns (uint256);
    
        function balanceOf(address who) public view returns (uint256);
        
        function transfer(address to, uint256 value) public returns (bool);
        
      
    }
    
    /**
     * @title ERC20Detailed token
     * @dev The decimals are only for visualization purposes.
     * All the operations are done using the smallest and indivisible token unit,
     * just as on Ethereum all the operations are done in wei.
     */
    contract ERC20Detailed is IERC20 {
        string private _name;
        string private _symbol;
        uint8 private _decimals;
    
        constructor (string memory name, string memory symbol, uint8 decimals) public {
            _name = name;
            _symbol = symbol;
            _decimals = decimals;
        }
    
        /**
         * @return the name of the token.
         */
        function name() public view returns (string memory) {
            return _name;
        }
    
        /**
         * @return the symbol of the token.
         */
        function symbol() public view returns (string memory) {
            return _symbol;
        }
    
        /**
         * @return the number of decimals of the token.
         */
        function decimals() public view returns (uint8) {
            return _decimals;
        }
    }
    
    interface IPack {
    
        function openChest(Pack.Type packType, address user, uint count) external returns (uint);
    
    }
    
    
    /**
     * @title Standard ERC20 token
     *
     * @dev Implementation of the basic standard token.
     * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
     * Originally based on code by FirstBlood:
     * https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
     *
     * This implementation emits additional Approval events, allowing applications to reconstruct the allowance status for
     * all accounts just by listening to said events. Note that this isn't required by the specification, and other
     * compliant implementations may not do it.
     */
    contract ERC20 is IERC20 {
        using SafeMath for uint256;
    
        mapping (address => uint256) private _balances;
    
        mapping (address => mapping (address => uint256)) private _allowed;
    
        uint256 private _totalSupply;
    
        /**
        * @dev Total number of tokens in existence
        */
        function totalSupply() public view returns (uint256) {
            return _totalSupply;
        }
    
        /**
        * @dev Gets the balance of the specified address.
        * @param owner The address to query the balance of.
        * @return An uint256 representing the amount owned by the passed address.
        */
        function balanceOf(address owner) public view returns (uint256) {
            return _balances[owner];
        }
    
        /**
         * @dev Function to check the amount of tokens that an owner allowed to a spender.
         * @param owner address The address which owns the funds.
         * @param spender address The address which will spend the funds.
         * @return A uint256 specifying the amount of tokens still available for the spender.
         */
        function allowance(address owner, address spender) public view returns (uint256) {
            return _allowed[owner][spender];
        }
    
        /**
        * @dev Transfer token for a specified address
        * @param to The address to transfer to.
        * @param value The amount to be transferred.
        */
        function transfer(address to, uint256 value) public returns (bool) {
            _transfer(msg.sender, to, value);
            return true;
        }
    
        /**
         * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
         * Beware that changing an allowance with this method brings the risk that someone may use both the old
         * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
         * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
         * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
         * @param spender The address which will spend the funds.
         * @param value The amount of tokens to be spent.
         */
        function approve(address spender, uint256 value) public returns (bool) {
            require(spender != address(0));
    
            _allowed[msg.sender][spender] = value;
            emit Approval(msg.sender, spender, value);
            return true;
        }
    
        /**
         * @dev Transfer tokens from one address to another.
         * Note that while this function emits an Approval event, this is not required as per the specification,
         * and other compliant implementations may not emit the event.
         * @param from address The address which you want to send tokens from
         * @param to address The address which you want to transfer to
         * @param value uint256 the amount of tokens to be transferred
         */
        function transferFrom(address from, address to, uint256 value) public returns (bool) {
            _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);
            _transfer(from, to, value);
            emit Approval(from, msg.sender, _allowed[from][msg.sender]);
            return true;
        }
    
        /**
         * @dev Increase the amount of tokens that an owner allowed to a spender.
         * approve should be called when allowed_[_spender] == 0. To increment
         * allowed value is better to use this function to avoid 2 calls (and wait until
         * the first transaction is mined)
         * From MonolithDAO Token.sol
         * Emits an Approval event.
         * @param spender The address which will spend the funds.
         * @param addedValue The amount of tokens to increase the allowance by.
         */
        function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
            require(spender != address(0));
    
            _allowed[msg.sender][spender] = _allowed[msg.sender][spender].add(addedValue);
            emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
            return true;
        }
    
        /**
         * @dev Decrease the amount of tokens that an owner allowed to a spender.
         * approve should be called when allowed_[_spender] == 0. To decrement
         * allowed value is better to use this function to avoid 2 calls (and wait until
         * the first transaction is mined)
         * From MonolithDAO Token.sol
         * Emits an Approval event.
         * @param spender The address which will spend the funds.
         * @param subtractedValue The amount of tokens to decrease the allowance by.
         */
        function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
            require(spender != address(0));
    
            _allowed[msg.sender][spender] = _allowed[msg.sender][spender].sub(subtractedValue);
            emit Approval(msg.sender, spender, _allowed[msg.sender][spender]);
            return true;
        }
    
        /**
        * @dev Transfer token for a specified addresses
        * @param from The address to transfer from.
        * @param to The address to transfer to.
        * @param value The amount to be transferred.
        */
        function _transfer(address from, address to, uint256 value) internal {
            require(to != address(0));
    
            _balances[from] = _balances[from].sub(value);
            _balances[to] = _balances[to].add(value);
            emit Transfer(from, to, value);
        }
    
        /**
         * @dev Internal function that mints an amount of the token and assigns it to
         * an account. This encapsulates the modification of balances such that the
         * proper events are emitted.
         * @param account The account that will receive the created tokens.
         * @param value The amount that will be created.
         */
        function _mint(address account, uint256 value) internal {
            require(account != address(0));
    
            _totalSupply = _totalSupply.add(value);
            _balances[account] = _balances[account].add(value);
            emit Transfer(address(0), account, value);
        }
    
        /**
         * @dev Internal function that burns an amount of the token of a given
         * account.
         * @param account The account whose tokens will be burnt.
         * @param value The amount that will be burnt.
         */
        function _burn(address account, uint256 value) internal {
            require(account != address(0));
    
            _totalSupply = _totalSupply.sub(value);
            _balances[account] = _balances[account].sub(value);
            emit Transfer(account, address(0), value);
        }
    
        /**
         * @dev Internal function that burns an amount of the token of a given
         * account, deducting from the sender's allowance for said account. Uses the
         * internal burn function.
         * Emits an Approval event (reflecting the reduced allowance).
         * @param account The account whose tokens will be burnt.
         * @param value The amount that will be burnt.
         */
        function _burnFrom(address account, uint256 value) internal {
            _allowed[account][msg.sender] = _allowed[account][msg.sender].sub(value);
            _burn(account, value);
            emit Approval(account, msg.sender, _allowed[account][msg.sender]);
        }
    }
    
    /**
     * @title Burnable Token
     * @dev Token that can be irreversibly burned (destroyed).
     */
    contract ERC20Burnable is ERC20 {
        /**
         * @dev Burns a specific amount of tokens.
         * @param value The amount of token to be burned.
         */
        function burn(uint256 value) internal {
            _burn(msg.sender, value);
        }
    
        /**
         * @dev Burns a specific amount of tokens from the target address and decrements allowance
         * @param from address The address which you want to send tokens from
         * @param value uint256 The amount of token to be burned
         */
        function burnFrom(address from, uint256 value) internal {
            _burnFrom(from, value);
        }
    }
    
    
    
    
    
    
    
    contract Chest is Ownable, ERC20Detailed, ERC20Burnable {
    
        using SafeMath for uint;
    
        uint256 public cap;
        IProcessor public processor;
        IPack public pack;
        Pack.Type public packType;
        uint public price;
        bool public tradeable;
        uint256 public sold;
    
        event ChestsPurchased(address user, uint count, address referrer, uint paymentID);
    
        constructor(
            IPack _pack, Pack.Type _pt,
            uint _price, IProcessor _processor, uint _cap,
            string memory name, string memory sym
        ) public ERC20Detailed(name, sym, 0) {
            price = _price;
            cap = _cap;
            pack = _pack;
            packType = _pt;
            processor = _processor;
        }
    
        function purchase(uint count, address referrer) public payable {
            return purchaseFor(msg.sender, count, referrer);
        }
    
        function purchaseFor(address user, uint count, address referrer) public payable {
    
            _mint(user, count);
    
            uint paymentID = processor.processPayment.value(msg.value)(msg.sender, price, count, referrer);
            emit ChestsPurchased(user, count, referrer, paymentID);
        }
    
        function open(uint value) public payable returns (uint) {
            return openFor(msg.sender, value);
        }
    
        // can only open uint16 at a time
        function openFor(address user, uint value) public payable returns (uint) {
    
            require(value > 0, "must open at least one chest");
            // can only be done by those with authority to burn
            // I would expect burnFrom to work in both cases but doesn't work with Zeppelin implementation
            if (user == msg.sender) {
                burn(value);
            } else {
                burnFrom(user, value);
            }
    
            require(address(pack) != address(0), "pack must be set");
       
            return pack.openChest(packType, user, value);
        }
    
        function makeTradeable() public onlyOwner {
            tradeable = true;
        }
    
        function transfer(address to, uint256 value) public returns (bool) {
            require(tradeable, "not currently tradeable");
            return super.transfer(to, value);
        }
    
        function transferFrom(address from, address to, uint256 value) public returns (bool) {
            require(tradeable, "not currently tradeable");
            return super.transferFrom(from, to, value);
        }
    
        function _mint(address account, uint256 value) internal {
            sold = sold.add(value);
            if (cap > 0) {
                require(sold <= cap, "not enough space in cap");
            }
            super._mint(account, value);
        }
    
    }

    File 2 of 2: PackFive
    pragma solidity ^0.5.0;
    
    interface IProcessor {
    
        function processPayment(address user, uint cost, uint items, address referrer) external payable returns (uint id);
        
    }
    
    contract Pack {
    
        enum Type {
            Rare, Epic, Legendary, Shiny
        }
    
    }
    
    contract Ownable {
    
        address payable public owner;
    
        constructor() public {
            owner = msg.sender;
        }
    
        function setOwner(address payable _owner) public onlyOwner {
            owner = _owner;
        }
    
        function getOwner() public view returns (address payable) {
            return owner;
        }
    
        modifier onlyOwner {
            require(msg.sender == owner, "must be owner to call this function");
            _;
        }
    
    }
    
    // from OZ
    
    /**
     * @title SafeMath
     * @dev Math operations with safety checks that throw on error
     */
    library SafeMath {
    
        /**
        * @dev Multiplies two numbers, throws on overflow.
        */
        function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {
            // Gas optimization: this is cheaper than asserting 'a' not being zero, but the
            // benefit is lost if 'b' is also tested.
            // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
            if (a == 0) {
                return 0;
            }
    
            c = a * b;
            assert(c / a == b);
            return c;
        }
    
        /**
        * @dev Integer division of two numbers, truncating the quotient.
        */
        function div(uint256 a, uint256 b) internal pure returns (uint256) {
            // assert(b > 0); // Solidity automatically throws when dividing by 0
            // uint256 c = a / b;
            // assert(a == b * c + a % b); // There is no case in which this doesn't hold
            return a / b;
        }
    
        /**
        * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
        */
        function sub(uint256 a, uint256 b) internal pure returns (uint256) {
            assert(b <= a);
            return a - b;
        }
    
        /**
        * @dev Adds two numbers, throws on overflow.
        */
        function add(uint256 a, uint256 b) internal pure returns (uint256 c) {
            c = a + b;
            assert(c >= a);
            return c;
        }
    }
    
    // from OZ
    
    /**
     * @title SafeMath
     * @dev Math operations with safety checks that throw on error
     */
    library SafeMath64 {
    
        /**
        * @dev Multiplies two numbers, throws on overflow.
        */
        function mul(uint64 a, uint64 b) internal pure returns (uint64 c) {
            // Gas optimization: this is cheaper than asserting 'a' not being zero, but the
            // benefit is lost if 'b' is also tested.
            // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
            if (a == 0) {
                return 0;
            }
    
            c = a * b;
            assert(c / a == b);
            return c;
        }
    
        /**
        * @dev Integer division of two numbers, truncating the quotient.
        */
        function div(uint64 a, uint64 b) internal pure returns (uint64) {
            // assert(b > 0); // Solidity automatically throws when dividing by 0
            // uint64 c = a / b;
            // assert(a == b * c + a % b); // There is no case in which this doesn't hold
            return a / b;
        }
    
        /**
        * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
        */
        function sub(uint64 a, uint64 b) internal pure returns (uint64) {
            assert(b <= a);
            return a - b;
        }
    
        /**
        * @dev Adds two numbers, throws on overflow.
        */
        function add(uint64 a, uint64 b) internal pure returns (uint64 c) {
            c = a + b;
            assert(c >= a);
            return c;
        }
    }
    
    contract ICards {
    
        enum Rarity {
            Common, Rare, Epic, Legendary, Mythic
        }
    
        function getRandomCard(Rarity rarity, uint16 random) public view returns (uint16);
        function createCard(address user, uint16 proto, uint16 purity) public returns (uint);
    
    
    }
    
    
    contract RarityProvider {
    
        ICards cards;
    
        constructor(ICards _cards) public {
            cards = _cards;
        }
    
        struct RandomnessComponents {
            uint random;
            uint32 rarity;
            uint16 quality;
            uint16 purity;
            uint16 proto;
        }
    
        // return 'length' bytes of 'num' starting at 'start'
        function extract(uint num, uint length, uint start) internal pure returns (uint) {
            return (((1 << (length * 8)) - 1) & (num >> ((start - 1) * 8)));
        }
    
        // divides the random seed into components
        function getComponents(
            uint cardIndex, uint rand
        ) internal pure returns (
            RandomnessComponents memory
        ) {
            uint random = uint(keccak256(abi.encodePacked(cardIndex, rand)));
            return RandomnessComponents({
                random: random,
                rarity: uint32(extract(random, 4, 10) % 1000000),
                quality: uint16(extract(random, 2, 4) % 1000),
                purity: uint16(extract(random, 2, 6) % 1000),
                proto: uint16(extract(random, 2, 8) % (2**16-1))
            });
        }
    
        function getCardDetails(Pack.Type packType, uint cardIndex, uint result) internal view returns (uint16, uint16) {
            if (packType == Pack.Type.Shiny) {
                return _getShinyCardDetails(cardIndex, result);
            } else if (packType == Pack.Type.Legendary) {
                return _getLegendaryCardDetails(cardIndex, result);
            } else if (packType == Pack.Type.Epic) {
                return _getEpicCardDetails(cardIndex, result);
            }
            return _getRareCardDetails(cardIndex, result);
        }
    
        function _getShinyCardDetails(uint cardIndex, uint result) internal view returns (uint16 proto, uint16 purity) {
            
            RandomnessComponents memory rc = getComponents(cardIndex, result); 
    
            ICards.Rarity rarity;
    
            if (cardIndex % 5 == 0) {
                rarity = _getLegendaryPlusRarity(rc.rarity);
                purity = _getShinyPurityBase(rc.quality) + rc.purity;
            } else if (cardIndex % 5 == 1) {
                rarity = _getRarePlusRarity(rc.rarity);
                purity = _getPurityBase(rc.quality) + rc.purity;
            } else {
                rarity = _getCommonPlusRarity(rc.rarity);
                purity = _getPurityBase(rc.quality) + rc.purity;
            }
            proto = cards.getRandomCard(rarity, rc.proto);
            return (proto, purity);
        }
    
        function _getLegendaryCardDetails(uint cardIndex, uint result) internal view returns (uint16 proto, uint16 purity) {
            
            RandomnessComponents memory rc = getComponents(cardIndex, result);
    
            ICards.Rarity rarity;
    
            if (cardIndex % 5 == 0) {
                rarity = _getLegendaryPlusRarity(rc.rarity);
            } else if (cardIndex % 5 == 1) {
                rarity = _getRarePlusRarity(rc.rarity);
            } else {
                rarity = _getCommonPlusRarity(rc.rarity);
            }
    
            purity = _getPurityBase(rc.quality) + rc.purity;
        
            proto = cards.getRandomCard(rarity, rc.proto);
    
            return (proto, purity);
        }
    
    
        function _getEpicCardDetails(uint cardIndex, uint result) internal view returns (uint16 proto, uint16 purity) {
            
            RandomnessComponents memory rc = getComponents(cardIndex, result);
    
            ICards.Rarity rarity;
    
            if (cardIndex % 5 == 0) {
                rarity = _getEpicPlusRarity(rc.rarity);
            } else {
                rarity = _getCommonPlusRarity(rc.rarity);
            }
    
            purity = _getPurityBase(rc.quality) + rc.purity;
        
            proto = cards.getRandomCard(rarity, rc.proto);
    
            return (proto, purity);
        } 
    
        function _getRareCardDetails(uint cardIndex, uint result) internal view returns (uint16 proto, uint16 purity) {
    
            RandomnessComponents memory rc = getComponents(cardIndex, result);
    
            ICards.Rarity rarity;
    
            if (cardIndex % 5 == 0) {
                rarity = _getRarePlusRarity(rc.rarity);
            } else {
                rarity = _getCommonPlusRarity(rc.rarity);
            }
    
            purity = _getPurityBase(rc.quality) + rc.purity;
        
            proto = cards.getRandomCard(rarity, rc.proto);
            return (proto, purity);
        }  
    
    
        function _getCommonPlusRarity(uint32 rand) internal pure returns (ICards.Rarity) {
            if (rand == 999999) {
                return ICards.Rarity.Mythic;
            } else if (rand >= 998345) {
                return ICards.Rarity.Legendary;
            } else if (rand >= 986765) {
                return ICards.Rarity.Epic;
            } else if (rand >= 924890) {
                return ICards.Rarity.Rare;
            } else {
                return ICards.Rarity.Common;
            }
        }
    
        function _getRarePlusRarity(uint32 rand) internal pure returns (ICards.Rarity) {
            if (rand == 999999) {
                return ICards.Rarity.Mythic;
            } else if (rand >= 981615) {
                return ICards.Rarity.Legendary;
            } else if (rand >= 852940) {
                return ICards.Rarity.Epic;
            } else {
                return ICards.Rarity.Rare;
            } 
        }
    
        function _getEpicPlusRarity(uint32 rand) internal pure returns (ICards.Rarity) {
            if (rand == 999999) {
                return ICards.Rarity.Mythic;
            } else if (rand >= 981615) {
                return ICards.Rarity.Legendary;
            } else {
                return ICards.Rarity.Epic;
            }
        }
    
        function _getLegendaryPlusRarity(uint32 rand) internal pure returns (ICards.Rarity) {
            if (rand == 999999) {
                return ICards.Rarity.Mythic;
            } else {
                return ICards.Rarity.Legendary;
            } 
        }
    
        // store purity and shine as one number to save users gas
        function _getPurityBase(uint16 randOne) internal pure returns (uint16) {
            if (randOne >= 998) {
                return 3000;
            } else if (randOne >= 988) {
                return 2000;
            } else if (randOne >= 938) {
                return 1000;
            }
            return 0;
        }
    
        function _getShinyPurityBase(uint16 randOne) internal pure returns (uint16) {
            if (randOne >= 998) {
                return 3000;
            } else if (randOne >= 748) {
                return 2000;
            } else {
                return 1000;
            }
        }
    
        function getShine(uint16 purity) public pure returns (uint8) {
            return uint8(purity / 1000);
        }
    
    }
    
    
    
    
    
    
    contract PackFive is Ownable, RarityProvider {
    
        using SafeMath for uint;
        using SafeMath64 for uint64;
    
        // fired after user purchases count packs, producing purchase with id
        event PacksPurchased(uint indexed paymentID, uint indexed id, Pack.Type indexed packType, address user, uint count, uint64 lockup);
        // fired after the callback transaction is successful, replaces RandomnessReceived
        event CallbackMade(uint indexed id, address indexed user, uint count, uint randomness);
        // fired after a recommit for a purchase
        event Recommit(uint indexed id, Pack.Type indexed packType, address indexed user, uint count, uint64 lockup);
        // fired after a card is activated, replaces PacksOpened
        event CardActivated(uint indexed purchaseID, uint cardIndex, uint indexed cardID, uint16 proto, uint16 purity);
        // fired after a chest is opened
        event ChestsOpened(uint indexed id, Pack.Type indexed packType, address indexed user, uint count, uint packCount);
        // fired after a purchase is recorded (either buying packs directly or indirectly)
        // callback sentinels should watch this event
        event PurchaseRecorded(uint indexed id, Pack.Type indexed packType, address indexed user, uint count, uint64 lockup);
        // fired after a purchase is revoked
        event PurchaseRevoked(uint indexed paymentID, address indexed revoker);
        // fired when a new pack is added
        event PackAdded(Pack.Type indexed packType, uint price, address chest);
    
        struct Purchase {
            uint count;
            uint randomness;
            uint[] state;
            Pack.Type packType;
            uint64 commit;
            uint64 lockup;
            bool revoked;
            address user;
        }
    
        struct PackInstance {
            uint price;
            uint chestSize;
            address token;
        }
    
        Purchase[] public purchases;
        IProcessor public processor;
        mapping(uint => PackInstance) public packs;
        mapping(address => bool) public canLockup;
        mapping(address => bool) public canRevoke;
        uint public commitLag = 0;
        // TODO: check this fits under mainnet gas limit
        uint16 public activationLimit = 40;
        // override switch in case of contract upgrade etc
        bool public canActivate = false;
        // maximum lockup length in blocks
        uint64 public maxLockup = 600000;
    
        constructor(ICards _cards, IProcessor _processor) public RarityProvider(_cards) {
            processor = _processor;
        }
    
        // == Admin Functions ==
        function setCanLockup(address user, bool can) public onlyOwner {
            canLockup[user] = can;
        }
    
        function setCanRevoke(address user, bool can) public onlyOwner {
            canRevoke[user] = can;
        }
    
        function setCommitLag(uint lag) public onlyOwner {
            require(commitLag < 100, "can't have a commit lag of >100 blocks");
            commitLag = lag;
        }
    
        function setActivationLimit(uint16 _limit) public onlyOwner {
            activationLimit = _limit;
        }
    
        function setMaxLockup(uint64 _max) public onlyOwner {
            maxLockup = _max;
        }
    
        function setPack(
            Pack.Type packType, uint price, address chest, uint chestSize
        ) public onlyOwner {
    
            PackInstance memory p = getPack(packType);
            require(p.token == address(0) && p.price == 0, "pack instance already set");
    
            require(price > 0, "price cannot be zero");
            require(price % 100 == 0, "price must be a multiple of 100 wei");
            require(address(processor) != address(0), "processor must be set");
    
            packs[uint(packType)] = PackInstance({
                token: chest,
                price: price,
                chestSize: chestSize
            });
    
            emit PackAdded(packType, price, chest);
        }
    
        function setActivate(bool can) public onlyOwner {
            canActivate = can;
        }
    
        function canActivatePurchase(uint id) public view returns (bool) {
            if (!canActivate) {
                return false;
            }
            Purchase memory p = purchases[id];
            if (p.lockup > 0) {
                if (inLockupPeriod(p)) {
                    return false;
                }
                return !p.revoked;
            }
            return true;
        }
    
        function revoke(uint id) public {
            require(canRevoke[msg.sender], "sender not approved to revoke");
            Purchase storage p = purchases[id];
            require(!p.revoked, "must not be revoked already");
            require(p.lockup > 0, "must have lockup set");
            require(inLockupPeriod(p), "must be in lockup period");
            p.revoked = true;
            emit PurchaseRevoked(id, msg.sender);
        }
    
        // == User Functions ==
    
        function purchase(Pack.Type packType, uint16 count, address referrer) public payable returns (uint) {
            return purchaseFor(packType, msg.sender, count, referrer, 0);
        }
    
        function purchaseFor(Pack.Type packType, address user, uint16 count, address referrer, uint64 lockup) public payable returns (uint) {
    
            PackInstance memory pack = getPack(packType);
    
            uint purchaseID = _recordPurchase(packType, user, count, lockup);
        
            uint paymentID = processor.processPayment.value(msg.value)(msg.sender, pack.price, count, referrer);
            
            emit PacksPurchased(paymentID, purchaseID, packType, user, count, lockup);
    
            return purchaseID;
        }
    
        function activateMultiple(uint[] memory pIDs, uint[] memory cardIndices)
            public returns (uint[] memory ids, uint16[] memory protos, uint16[] memory purities) {
            uint len = pIDs.length;
            require(len > 0, "can't activate no cards");
            require(len <= activationLimit, "can't activate more than the activation limit");
            require(len == cardIndices.length, "must have the same length");
            ids = new uint[](len);
            protos = new uint16[](len);
            purities = new uint16[](len);
            for (uint i = 0; i < len; i++) {
                (ids[i], protos[i], purities[i]) = activate(pIDs[i], cardIndices[i]);
            }
            return (ids, protos, purities);
        }
    
        function activate(uint purchaseID, uint cardIndex) public returns (uint id, uint16 proto, uint16 purity) {
            
            require(canActivatePurchase(purchaseID), "can't activate purchase");
            Purchase storage p = purchases[purchaseID];
            
            require(p.randomness != 0, "must have been a callback");
            uint cardCount = uint(p.count).mul(5);
            require(cardIndex < cardCount, "not a valid card index");
            uint bit = getStateBit(purchaseID, cardIndex);
            // can only activate each card once
            require(bit == 0, "card has already been activated");
            uint x = cardIndex.div(256);
            uint pos = cardIndex % 256;
            // mark the card as activated by flipping the relevant bit
            p.state[x] ^= uint(1) << pos;
            // create the card
            (proto, purity) = getCardDetails(p.packType, cardIndex, p.randomness);
            id = cards.createCard(p.user, proto, purity);
            emit CardActivated(purchaseID, cardIndex, id, proto, purity);
            return (id, proto, purity);
        }
    
        // 'open' a number of chest tokens
        function openChest(Pack.Type packType, address user, uint count) public returns (uint) {
            
            PackInstance memory pack = getPack(packType);
    
            require(msg.sender == pack.token, "can only open from the actual token packs");
    
            uint packCount = count.mul(pack.chestSize);
            
            uint id = _recordPurchase(packType, user, packCount, 0);
    
            emit ChestsOpened(id, packType, user, count, packCount);
    
            return id;
        }
    
        function _recordPurchase(Pack.Type packType, address user, uint count, uint64 lockup) internal returns (uint) {
    
            if (lockup != 0) {
                require(lockup < maxLockup, "lockup must be lower than maximum");
                require(canLockup[msg.sender], "only some people can lockup cards");
            }
            
            Purchase memory p = Purchase({
                user: user,
                count: count,
                commit: getCommitBlock(),
                randomness: 0,
                packType: packType,
                state: new uint256[](getStateSize(count)),
                lockup: lockup,
                revoked: false
            });
    
            uint id = purchases.push(p).sub(1);
    
            emit PurchaseRecorded(id, packType, user, count, lockup);
            return id;
        }
    
        // can be called by anybody
        function callback(uint id) public {
    
            Purchase storage p = purchases[id];
    
            require(p.randomness == 0, "randomness already set");
    
            require(uint64(block.number) > p.commit, "cannot callback before commit");
    
            // must be within last 256 blocks, otherwise recommit
            require(p.commit.add(uint64(256)) >= block.number, "must recommit");
    
            bytes32 bhash = blockhash(p.commit);
    
            require(uint(bhash) != 0, "blockhash must not be zero");
    
            // only use properties which can't be altered by the user
            // id and factory are determined before the reveal
            // 'last' determined param must be random
            p.randomness = uint(keccak256(abi.encodePacked(id, bhash, address(this))));
    
            emit CallbackMade(id, p.user, p.count, p.randomness);
        }
    
        // can recommit
        // this gives you more chances
        // if no-one else sends the callback (should never happen)
        // still only get a random extra chance
        function recommit(uint id) public {
            Purchase storage p = purchases[id];
            require(p.randomness == 0, "randomness already set");
            require(block.number >= p.commit.add(uint64(256)), "no need to recommit");
            p.commit = getCommitBlock();
            emit Recommit(id, p.packType, p.user, p.count, p.lockup);
        }
    
        // == View Functions ==
    
        function getCommitBlock() internal view returns (uint64) {
            return uint64(block.number.add(commitLag));
        }
    
        function getStateSize(uint count) public pure returns (uint) {
            return count.mul(5).sub(1).div(256).add(1);
        }
    
        function getPurchaseState(uint purchaseID) public view returns (uint[] memory state) {
            require(purchases.length > purchaseID, "invalid purchase id");
            Purchase memory p = purchases[purchaseID];
            return p.state;
        }
        
        function getPackDetails(Pack.Type packType) public view returns (address token, uint price) {
            PackInstance memory p = getPack(packType);
            return (p.token, p.price);
        }
    
        function getPack(Pack.Type packType) internal view returns (PackInstance memory) {
            return packs[uint(packType)];
        }
    
        function getPrice(Pack.Type packType) public view returns (uint) {
            PackInstance memory p = getPack(packType);
            require(p.price != 0, "price is not yet set");
            return p.price;
        }
    
        function getChestSize(Pack.Type packType) public view returns (uint) {
            PackInstance memory p = getPack(packType);
            require(p.chestSize != 0, "chest size is not yet set");
            return p.chestSize;
        }
    
        function isActivated(uint purchaseID, uint cardIndex) public view returns (bool) {
            return getStateBit(purchaseID, cardIndex) != 0;
        }
    
        function getStateBit(uint purchaseID, uint cardIndex) public view returns (uint) {
            Purchase memory p = purchases[purchaseID];
            uint x = cardIndex.div(256);
            uint slot = p.state[x];
            uint pos = cardIndex % 256;
            uint bit = (slot >> pos) & uint(1);
            return bit;
        }
    
        function predictPacks(uint id) external view returns (uint16[] memory protos, uint16[] memory purities) {
    
            Purchase memory p = purchases[id];
    
            require(p.randomness != 0, "randomness not yet set");
    
            uint result = p.randomness;
    
            uint cardCount = uint(p.count).mul(5);
    
            purities = new uint16[](cardCount);
            protos = new uint16[](cardCount);
    
            for (uint i = 0; i < cardCount; i++) {
                (protos[i], purities[i]) = getCardDetails(p.packType, i, result);
            }
    
            return (protos, purities);
        }
     
        function inLockupPeriod(Purchase memory p) internal view returns (bool) {
            return p.commit.add(p.lockup) >= block.number;
        }
    
    }