2020-04-10 10:14:50 +00:00
const TestNFT = artifacts . require ( 'TestNFT' ) ;
2020-09-30 10:11:56 +00:00
const NFTBucket = artifacts . require ( 'NFTBucket' ) ;
2020-04-10 10:14:50 +00:00
const NFTBucketFactory = artifacts . require ( 'NFTBucketFactory' ) ;
const TOTAL _SUPPLY = 10000 ;
const GIFT _AMOUNT = 10 ;
const REDEEM _CODE = web3 . utils . sha3 ( "hello world" ) ;
const NOW = Math . round ( new Date ( ) . getTime ( ) / 1000 ) ;
2020-04-23 12:51:05 +00:00
const START _TIME = NOW - 1 ;
2020-04-10 10:14:50 +00:00
const EXPIRATION _TIME = NOW + 60 * 60 * 24 ; // in 24 hours
2020-05-04 06:04:30 +00:00
const MAX _TX _DELAY _BLOCKS = 10 ;
2020-04-10 10:14:50 +00:00
async function signRedeem ( contractAddress , signer , message ) {
const result = await web3 . eth . net . getId ( ) ;
let chainId = parseInt ( result ) ;
//FIXME: in tests, getChainID in the contract returns 1 so we hardcode it here to 1.
chainId = 1 ;
const domain = [
{ name : "name" , type : "string" } ,
{ name : "version" , type : "string" } ,
{ name : "chainId" , type : "uint256" } ,
{ name : "verifyingContract" , type : "address" }
] ;
const redeem = [
{ name : "blockNumber" , type : "uint256" } ,
{ name : "blockHash" , type : "bytes32" } ,
{ name : "receiver" , type : "address" } ,
{ name : "code" , type : "bytes32" } ,
] ;
const domainData = {
2020-04-28 09:36:34 +00:00
name : "KeycardNFTBucket" ,
2020-04-10 10:14:50 +00:00
version : "1" ,
chainId : chainId ,
verifyingContract : contractAddress
} ;
const data = {
types : {
EIP712Domain : domain ,
Redeem : redeem ,
} ,
primaryType : "Redeem" ,
domain : domainData ,
message : message
} ;
return new Promise ( ( resolve , reject ) => {
2020-09-30 10:11:56 +00:00
web3 . currentProvider . send ( {
2020-04-10 10:14:50 +00:00
jsonrpc : '2.0' ,
id : Date . now ( ) . toString ( ) . substring ( 9 ) ,
method : "eth_signTypedData" ,
params : [ signer , data ] ,
from : signer
} , ( error , res ) => {
if ( error ) {
return reject ( error ) ;
}
resolve ( res . result ) ;
} ) ;
} ) ;
}
function mineAt ( timestamp ) {
return new Promise ( ( resolve , reject ) => {
2020-09-30 10:11:56 +00:00
web3 . currentProvider . send ( {
2020-04-10 10:14:50 +00:00
jsonrpc : '2.0' ,
method : "evm_mine" ,
params : [ timestamp ] ,
id : Date . now ( ) . toString ( ) . substring ( 9 )
} , ( error , res ) => {
if ( error ) {
return reject ( error ) ;
}
resolve ( res . result ) ;
} ) ;
} ) ;
}
if ( assert . match === undefined ) {
assert . match = ( message , pattern ) => {
assert ( pattern . test ( message ) , ` ${ message } doesn't match ${ pattern } ` ) ;
}
}
contract ( "NFTBucket" , function ( ) {
2020-09-30 10:11:56 +00:00
let bucketInstance ,
factoryInstance ,
tokenInstance ,
shop ,
user ,
relayer ,
keycard _1 ,
keycard _2 ,
keycard _3 ;
before ( async ( ) => {
const accounts = await web3 . eth . getAccounts ( ) ;
shop = accounts [ 0 ] ;
user = accounts [ 1 ] ;
relayer = accounts [ 2 ] ;
keycard _1 = accounts [ 3 ] ;
keycard _2 = accounts [ 4 ] ;
keycard _3 = accounts [ 5 ] ;
const deployedTestToken = await TestNFT . deployed ( ) ;
tokenInstance = new web3 . eth . Contract ( TestNFT . abi , deployedTestToken . address ) ;
} ) ;
2020-04-10 10:14:50 +00:00
it ( "deploy factory" , async ( ) => {
2020-09-30 10:11:56 +00:00
const contract = new web3 . eth . Contract ( NFTBucketFactory . abi ) ;
const deploy = contract . deploy ( { data : NFTBucketFactory . bytecode } ) ;
const gas = await deploy . estimateGas ( ) ;
const rec = await deploy . send ( {
from : shop ,
gas ,
2020-04-10 10:14:50 +00:00
} ) ;
2020-09-30 10:11:56 +00:00
factoryInstance = new web3 . eth . Contract ( NFTBucketFactory . abi , rec . options . address ) ;
2020-04-10 10:14:50 +00:00
} ) ;
it ( "deploy bucket" , async ( ) => {
2020-09-30 10:11:56 +00:00
const instance = new web3 . eth . Contract ( NFTBucket . abi ) ;
const deploy = instance . deploy ( {
data : NFTBucket . bytecode ,
arguments : [ tokenInstance . options . address , START _TIME , EXPIRATION _TIME , MAX _TX _DELAY _BLOCKS ]
2020-04-10 10:14:50 +00:00
} ) ;
const gas = await deploy . estimateGas ( ) ;
2020-09-30 10:11:56 +00:00
const rec = await deploy . send ( {
from : shop ,
gas ,
} ) ;
bucketInstance = new web3 . eth . Contract ( NFTBucket . abi , rec . options . address ) ;
2020-04-10 10:14:50 +00:00
} ) ;
it ( "deploy bucket via factory" , async ( ) => {
2020-09-30 10:11:56 +00:00
const create = factoryInstance . methods . create ( tokenInstance . _address , START _TIME , EXPIRATION _TIME , MAX _TX _DELAY _BLOCKS ) ;
2020-04-10 10:14:50 +00:00
const gas = await create . estimateGas ( ) ;
const receipt = await create . send ( {
from : shop ,
gas : gas ,
} ) ;
} ) ;
2020-04-29 10:50:25 +00:00
it ( "return correct bucket type" , async function ( ) {
2020-09-30 10:11:56 +00:00
let bucketType = await bucketInstance . methods . bucketType ( ) . call ( ) ;
2020-04-30 05:34:24 +00:00
assert . equal ( parseInt ( bucketType ) , 721 ) ;
2020-04-29 10:50:25 +00:00
} ) ;
2020-04-28 09:36:34 +00:00
function createRedeemableData ( recipient ) {
2020-04-10 10:14:50 +00:00
const redeemCodeHash = web3 . utils . sha3 ( REDEEM _CODE ) ;
return recipient + redeemCodeHash . replace ( "0x" , "" ) ;
}
2020-04-28 09:36:34 +00:00
async function checkRedeemable ( recipient , tokenID ) {
2020-09-30 10:11:56 +00:00
let redeemable = await bucketInstance . methods . redeemables ( recipient ) . call ( ) ;
2020-04-28 09:36:34 +00:00
assert . equal ( redeemable . recipient , recipient , "redeemable not found" ) ;
assert . equal ( parseInt ( redeemable . data ) , tokenID , "token ID does not match" ) ;
2020-09-30 10:11:56 +00:00
let tokenOwner = await tokenInstance . methods . ownerOf ( tokenID ) . call ( ) ;
assert . equal ( tokenOwner , bucketInstance . options . address , "token owner is wrong" ) ;
2020-04-10 10:14:50 +00:00
}
2020-04-28 09:36:34 +00:00
it ( "mint directly to redeemable" , async function ( ) {
2020-09-30 10:11:56 +00:00
const mint = tokenInstance . methods . mint ( bucketInstance . options . address , 42 , createRedeemableData ( keycard _1 ) ) ;
const gas = await mint . estimateGas ( ) ;
await mint . send ( {
2020-04-10 10:14:50 +00:00
from : shop ,
2020-09-30 10:11:56 +00:00
gas ,
2020-04-10 10:14:50 +00:00
} ) ;
2020-04-28 09:36:34 +00:00
await checkRedeemable ( keycard _1 , 42 ) ;
2020-04-10 10:14:50 +00:00
} ) ;
it ( "transfer token from shop" , async function ( ) {
2020-09-30 10:11:56 +00:00
const mint = tokenInstance . methods . mint ( shop , 0xcafe )
let gas = await mint . estimateGas ( ) ;
await mint . send ( {
from : shop ,
gas ,
} ) ;
2020-04-10 10:14:50 +00:00
2020-09-30 10:11:56 +00:00
const transfer = tokenInstance . methods . safeTransferFrom ( shop , bucketInstance . options . address , 0xcafe , createRedeemableData ( keycard _2 ) )
gas = await transfer . estimateGas ( ) ;
await transfer . send ( {
from : shop ,
gas ,
} ) ;
2020-04-28 09:36:34 +00:00
await checkRedeemable ( keycard _2 , 0xcafe ) ;
2020-04-10 10:14:50 +00:00
} ) ;
2020-04-28 09:36:34 +00:00
it ( "cannot create two redeemables for the same recipient" , async function ( ) {
2020-09-30 10:11:56 +00:00
const mint = await tokenInstance . methods . mint ( shop , 43 )
const gas = await mint . estimateGas ( ) ;
await mint . send ( {
from : shop ,
gas ,
} ) ;
2020-04-10 10:14:50 +00:00
try {
2020-09-30 10:11:56 +00:00
const transfer = tokenInstance . methods . safeTransferFrom ( shop , bucketInstance . options . address , 43 , createRedeemableData ( keycard _2 ) )
await transfer . send ( {
from : shop ,
gas ,
} ) ;
2020-04-10 10:14:50 +00:00
assert . fail ( "transfer should have failed" ) ;
} catch ( e ) {
assert . match ( e . message , /already used/ ) ;
}
} ) ;
2020-04-28 09:36:34 +00:00
it ( "cannot create two redeemables for the same token" , async function ( ) {
2020-04-13 08:46:09 +00:00
try {
2020-09-30 10:11:56 +00:00
const received = bucketInstance . methods . onERC721Received ( shop , shop , 0xcafe , createRedeemableData ( keycard _3 ) )
const gas = await received . estimateGas ( ) ;
await send ( {
from : shop ,
gas ,
} ) ;
2020-04-13 08:46:09 +00:00
assert . fail ( "transfer should have failed" ) ;
} catch ( e ) {
assert . match ( e . message , /only the NFT/ ) ;
}
2020-04-23 11:42:08 +00:00
} ) ;
2020-04-13 08:46:09 +00:00
2020-04-10 10:14:50 +00:00
async function testRedeem ( receiver , recipient , signer , relayer , redeemCode , blockNumber , blockHash ) {
2020-09-30 10:11:56 +00:00
let redeemable = await bucketInstance . methods . redeemables ( recipient ) . call ( ) ;
2020-04-28 09:36:34 +00:00
const tokenID = redeemable . data ;
2020-04-10 10:14:50 +00:00
const message = {
blockNumber : blockNumber ,
blockHash : blockHash ,
receiver : receiver ,
code : redeemCode ,
} ;
2020-09-30 10:11:56 +00:00
const sig = await signRedeem ( bucketInstance . options . address , signer , message ) ;
const redeem = bucketInstance . methods . redeem ( message , sig ) ;
2020-04-10 10:14:50 +00:00
const redeemGas = await redeem . estimateGas ( ) ;
2020-04-29 10:33:14 +00:00
let receipt = await redeem . send ( {
2020-04-10 10:14:50 +00:00
from : relayer ,
gas : redeemGas ,
} ) ;
2020-04-29 10:33:14 +00:00
assert . equal ( receipt . events . Redeemed . returnValues . recipient , recipient ) ;
assert . equal ( receipt . events . Redeemed . returnValues . data , tokenID ) ;
2020-09-30 10:11:56 +00:00
let tokenOwner = await tokenInstance . methods . ownerOf ( tokenID ) . call ( ) ;
2020-04-10 10:14:50 +00:00
assert . equal ( tokenOwner , receiver , ` Token owner is ${ tokenOwner } instead of the expected ${ receiver } ` ) ;
}
2020-04-23 12:51:05 +00:00
it ( "cannot redeem before the start date" , async function ( ) {
const block = await web3 . eth . getBlock ( "latest" ) ;
await mineAt ( START _TIME ) ;
try {
await testRedeem ( user , keycard _1 , keycard _1 , relayer , REDEEM _CODE , block . number , block . hash ) ;
assert . fail ( "redeem should have failed" ) ;
} catch ( e ) {
assert . match ( e . message , /not yet started/ ) ;
}
} ) ;
2020-04-10 10:14:50 +00:00
it ( "cannot redeem after expiration date" , async function ( ) {
const block = await web3 . eth . getBlock ( "latest" ) ;
await mineAt ( EXPIRATION _TIME ) ;
try {
await testRedeem ( user , keycard _1 , keycard _1 , relayer , REDEEM _CODE , block . number , block . hash ) ;
assert . fail ( "redeem should have failed" ) ;
} catch ( e ) {
assert . match ( e . message , /expired/ ) ;
}
} ) ;
it ( "cannot redeem with invalid code" , async function ( ) {
const block = await web3 . eth . getBlock ( "latest" ) ;
await mineAt ( NOW ) ;
try {
await testRedeem ( user , keycard _1 , keycard _1 , relayer , web3 . utils . sha3 ( "bad-code" ) , block . number , block . hash ) ;
assert . fail ( "redeem should have failed" ) ;
} catch ( e ) {
assert . match ( e . message , /invalid code/ ) ;
}
} ) ;
it ( "cannot redeem with invalid recipient" , async function ( ) {
const block = await web3 . eth . getBlock ( "latest" ) ;
await mineAt ( NOW ) ;
try {
await testRedeem ( user , keycard _1 , keycard _3 , relayer , REDEEM _CODE , block . number , block . hash ) ;
assert . fail ( "redeem should have failed" ) ;
} catch ( e ) {
assert . match ( e . message , /not found/ ) ;
}
} ) ;
it ( "cannot redeem with a block in the future" , async function ( ) {
const block = await web3 . eth . getBlock ( "latest" ) ;
await mineAt ( NOW ) ;
try {
await testRedeem ( user , keycard _1 , keycard _1 , relayer , REDEEM _CODE , ( block . number + 2 ) , "0x0000000000000000000000000000000000000000000000000000000000000000" ) ;
} catch ( e ) {
assert . match ( e . message , /future/ ) ;
}
} ) ;
it ( "cannot redeem with an old block" , async function ( ) {
const currentBlock = await web3 . eth . getBlock ( "latest" ) ;
const block = await web3 . eth . getBlock ( currentBlock . number - 10 ) ;
await mineAt ( NOW ) ;
try {
await testRedeem ( user , keycard _1 , keycard _1 , relayer , REDEEM _CODE , block . number , block . hash ) ;
} catch ( e ) {
assert . match ( e . message , /too old/ ) ;
}
} ) ;
it ( "cannot redeem with an invalid hash" , async function ( ) {
const block = await web3 . eth . getBlock ( "latest" ) ;
await mineAt ( NOW ) ;
try {
await testRedeem ( user , keycard _1 , keycard _1 , relayer , REDEEM _CODE , block . number , "0x0000000000000000000000000000000000000000000000000000000000000000" ) ;
} catch ( e ) {
assert . match ( e . message , /invalid block hash/ ) ;
}
2020-04-23 11:42:08 +00:00
} ) ;
2020-04-10 10:14:50 +00:00
it ( "can redeem before expiration date" , async function ( ) {
const block = await web3 . eth . getBlock ( "latest" ) ;
await mineAt ( NOW ) ;
await testRedeem ( user , keycard _1 , keycard _1 , relayer , REDEEM _CODE , block . number , block . hash ) ;
} ) ;
async function testKill ( ) {
2020-09-30 10:11:56 +00:00
assert ( ! await tokenInstance . methods . isApprovedForAll ( bucketInstance . options . address , shop ) . call ( ) , ` ${ shop } should not be the operator of bucket's tokens ` ) ;
const kill = bucketInstance . methods . kill ( ) ;
const gas = await kill . estimateGas ( ) ;
await kill . send ( {
from : shop ,
gas ,
} ) ;
assert ( await tokenInstance . methods . isApprovedForAll ( bucketInstance . options . address , shop ) . call ( ) , ` ${ shop } should become the operator of the destroyed bucket's tokens ` ) ;
2020-04-10 10:14:50 +00:00
}
it ( "shop cannot kill contract before expirationTime" , async function ( ) {
await mineAt ( NOW ) ;
try {
await testKill ( ) ;
assert . fail ( "redeem should have failed" ) ;
} catch ( e ) {
assert . match ( e . message , /not expired yet/ ) ;
}
} ) ;
it ( "shop can kill contract after expirationTime" , async function ( ) {
await mineAt ( EXPIRATION _TIME ) ;
await testKill ( ) ;
2020-04-23 11:42:08 +00:00
await mineAt ( NOW ) ;
2020-04-10 10:14:50 +00:00
} ) ;
} ) ;