Merge pull request #22 from status-im/gift-to-erc20-redeemable

remove gift terminology
This commit is contained in:
Bitgamma 2020-04-28 13:16:54 +03:00 committed by GitHub
commit a7e86d8652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 194 additions and 194 deletions

View File

@ -1,60 +1,60 @@
import { RootState } from '../reducers'; import { RootState } from '../reducers';
import GiftBucket from '../../../embarkArtifacts/contracts/GiftBucket'; import ERC20Bucket from '../../../embarkArtifacts/contracts/ERC20Bucket';
import IERC20Detailed from '../../../embarkArtifacts/contracts/IERC20Detailed'; import IERC20Detailed from '../../../embarkArtifacts/contracts/IERC20Detailed';
import { config } from "../config"; import { config } from "../config";
import { Contract } from 'web3-eth-contract'; import { Contract } from 'web3-eth-contract';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
export const ERROR_GIFT_NOT_FOUND = "ERROR_GIFT_NOT_FOUND"; export const ERROR_REDEEMABLE_NOT_FOUND = "ERROR_REDEEMABLE_NOT_FOUND";
export interface ErrGiftNotFound { export interface ErrRedeemableNotFound {
type: typeof ERROR_GIFT_NOT_FOUND type: typeof ERROR_REDEEMABLE_NOT_FOUND
} }
export const ERROR_LOADING_GIFT = "ERROR_LOADING_GIFT"; export const ERROR_LOADING_REDEEMABLE = "ERROR_LOADING_REDEEMABLE";
export interface ErrLoadingGift { export interface ErrLoadingRedeemable {
type: typeof ERROR_LOADING_GIFT type: typeof ERROR_LOADING_REDEEMABLE
message: string message: string
} }
export type BucketErrors = export type BucketErrors =
ErrGiftNotFound | ErrRedeemableNotFound |
ErrLoadingGift; ErrLoadingRedeemable;
const errGiftNotFound = (): ErrGiftNotFound => ({ const errRedeemableNotFound = (): ErrRedeemableNotFound => ({
type: ERROR_GIFT_NOT_FOUND, type: ERROR_REDEEMABLE_NOT_FOUND,
}); });
const errLoadingGift = (message: string): ErrLoadingGift => ({ const errLoadingRedeemable = (message: string): ErrLoadingRedeemable => ({
type: ERROR_LOADING_GIFT, type: ERROR_LOADING_REDEEMABLE,
message, message,
}); });
export const BUCKET_GIFT_LOADING = "BUCKET_GIFT_LOADING"; export const BUCKET_REDEEMABLE_LOADING = "BUCKET_REDEEMABLE_LOADING";
export interface BucketGiftLoadingAction { export interface BucketRedeemableLoadingAction {
type: typeof BUCKET_GIFT_LOADING type: typeof BUCKET_REDEEMABLE_LOADING
address: string address: string
recipient: string recipient: string
} }
export const BUCKET_GIFT_LOADING_ERROR = "BUCKET_GIFT_LOADING_ERROR"; export const BUCKET_REDEEMABLE_LOADING_ERROR = "BUCKET_REDEEMABLE_LOADING_ERROR";
export interface BucketGiftLoadingErrorAction { export interface BucketRedeemableLoadingErrorAction {
type: typeof BUCKET_GIFT_LOADING_ERROR type: typeof BUCKET_REDEEMABLE_LOADING_ERROR
error: ErrLoadingGift error: ErrLoadingRedeemable
} }
export const BUCKET_GIFT_LOADED = "BUCKET_GIFT_LOADED"; export const BUCKET_REDEEMABLE_LOADED = "BUCKET_REDEEMABLE_LOADED";
export interface BucketGiftLoadedAction { export interface BucketRedeemableLoadedAction {
type: typeof BUCKET_GIFT_LOADED type: typeof BUCKET_REDEEMABLE_LOADED
expirationTime: number expirationTime: number
recipient: string recipient: string
amount: string amount: string
codeHash: string codeHash: string
} }
export const BUCKET_GIFT_NOT_FOUND = "BUCKET_GIFT_NOT_FOUND"; export const BUCKET_REDEEMABLE_NOT_FOUND = "BUCKET_REDEEMABLE_NOT_FOUND";
export interface BucketGiftNotFoundAction { export interface BucketRedeemableNotFoundAction {
type: typeof BUCKET_GIFT_NOT_FOUND type: typeof BUCKET_REDEEMABLE_NOT_FOUND
error: ErrGiftNotFound error: ErrRedeemableNotFound
} }
export const BUCKET_TOKEN_LOADING = "BUCKET_TOKEN_LOADING"; export const BUCKET_TOKEN_LOADING = "BUCKET_TOKEN_LOADING";
@ -71,35 +71,35 @@ export interface BucketTokenLoadedAction {
} }
export type BucketActions = export type BucketActions =
BucketGiftLoadingAction | BucketRedeemableLoadingAction |
BucketGiftLoadingErrorAction | BucketRedeemableLoadingErrorAction |
BucketGiftLoadedAction | BucketRedeemableLoadedAction |
BucketGiftNotFoundAction | BucketRedeemableNotFoundAction |
BucketTokenLoadingAction | BucketTokenLoadingAction |
BucketTokenLoadedAction; BucketTokenLoadedAction;
export const loadingGift = (address: string, recipient: string): BucketGiftLoadingAction => ({ export const loadingRedeemable = (address: string, recipient: string): BucketRedeemableLoadingAction => ({
type: BUCKET_GIFT_LOADING, type: BUCKET_REDEEMABLE_LOADING,
address, address,
recipient, recipient,
}); });
export const giftLoaded = (expirationTime: number, recipient: string, amount: string, codeHash: string): BucketGiftLoadedAction => ({ export const redeemableLoaded = (expirationTime: number, recipient: string, amount: string, codeHash: string): BucketRedeemableLoadedAction => ({
type: BUCKET_GIFT_LOADED, type: BUCKET_REDEEMABLE_LOADED,
expirationTime, expirationTime,
recipient, recipient,
amount, amount,
codeHash, codeHash,
}); });
export const giftNotFound = (): BucketGiftNotFoundAction => ({ export const redeemableNotFound = (): BucketRedeemableNotFoundAction => ({
type: BUCKET_GIFT_NOT_FOUND, type: BUCKET_REDEEMABLE_NOT_FOUND,
error: errGiftNotFound(), error: errRedeemableNotFound(),
}); });
export const errorLoadingGift = (errorMessage: string): BucketGiftLoadingErrorAction => ({ export const errorLoadingRedeemable = (errorMessage: string): BucketRedeemableLoadingErrorAction => ({
type: BUCKET_GIFT_LOADING_ERROR, type: BUCKET_REDEEMABLE_LOADING_ERROR,
error: errLoadingGift(errorMessage), error: errLoadingRedeemable(errorMessage),
}); });
export const loadingToken = (address: string): BucketTokenLoadingAction => ({ export const loadingToken = (address: string): BucketTokenLoadingAction => ({
@ -114,7 +114,7 @@ export const tokenLoaded = (symbol: string, decimals: number): BucketTokenLoaded
}); });
export const newBucketContract = (address: string) => { export const newBucketContract = (address: string) => {
const bucketAbi = GiftBucket.options.jsonInterface; const bucketAbi = ERC20Bucket.options.jsonInterface;
const bucket = new config.web3!.eth.Contract(bucketAbi, address); const bucket = new config.web3!.eth.Contract(bucketAbi, address);
return bucket; return bucket;
} }
@ -125,22 +125,22 @@ const newERC20Contract = (address: string) => {
return erc20; return erc20;
} }
export const loadGift = (bucketAddress: string, recipientAddress: string) => { export const loadRedeemable = (bucketAddress: string, recipientAddress: string) => {
return async (dispatch: Dispatch, getState: () => RootState) => { return async (dispatch: Dispatch, getState: () => RootState) => {
dispatch(loadingGift(bucketAddress, recipientAddress)); dispatch(loadingRedeemable(bucketAddress, recipientAddress));
const bucket = newBucketContract(bucketAddress); const bucket = newBucketContract(bucketAddress);
const expirationTime = await bucket.methods.expirationTime().call(); const expirationTime = await bucket.methods.expirationTime().call();
bucket.methods.gifts(recipientAddress).call().then((result: any) => { bucket.methods.redeemables(recipientAddress).call().then((result: any) => {
const { recipient, amount, code } = result; const { recipient, amount, code } = result;
if (amount === "0") { if (amount === "0") {
dispatch(giftNotFound()) dispatch(redeemableNotFound())
return; return;
} }
dispatch(giftLoaded(expirationTime, recipient, amount, code)); dispatch(redeemableLoaded(expirationTime, recipient, amount, code));
dispatch<any>(loadToken(bucket)) dispatch<any>(loadToken(bucket))
}).catch(err => { }).catch(err => {
dispatch(errorLoadingGift(err)) dispatch(errorLoadingRedeemable(err))
console.error("err: ", err) console.error("err: ", err)
}) })
}; };

View File

@ -1,5 +1,5 @@
import { RootState } from '../reducers'; import { RootState } from '../reducers';
import GiftBucket from '../../../embarkArtifacts/contracts/GiftBucket'; import ERC20Bucket from '../../../embarkArtifacts/contracts/ERC20Bucket';
import IERC20Detailed from '../../../embarkArtifacts/contracts/IERC20Detailed'; import IERC20Detailed from '../../../embarkArtifacts/contracts/IERC20Detailed';
import { config } from "../config"; import { config } from "../config";
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
@ -154,7 +154,7 @@ async function signRedeem(web3Type: Web3Type, contractAddress: string, signer: s
]; ];
const domainData = { const domainData = {
name: "KeycardGift", name: "KeycardERC20Bucket",
version: "1", version: "1",
chainId: chainId, chainId: chainId,
verifyingContract: contractAddress verifyingContract: contractAddress

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import GiftBucketFactory from '../../../embarkArtifacts/contracts/GiftBucketFactory'; import ERC20BucketFactory from '../../../embarkArtifacts/contracts/ERC20BucketFactory';
import { import {
shallowEqual, shallowEqual,
useSelector, useSelector,
@ -40,7 +40,7 @@ export default function(ownProps: any) {
return <> return <>
Network ID: {props.networkID} <br /> Network ID: {props.networkID} <br />
Factory: {GiftBucketFactory.address} <br /> Factory: {ERC20BucketFactory.address} <br />
Web3 Type: {web3Type(props.type)} Web3 Type: {web3Type(props.type)}
<hr /> <hr />
<div> <div>

View File

@ -8,10 +8,10 @@ import {
} from 'react-redux'; } from 'react-redux';
import { redeemPath } from '../config'; import { redeemPath } from '../config';
import { import {
loadGift, loadRedeemable,
BucketErrors, BucketErrors,
ERROR_LOADING_GIFT, ERROR_LOADING_REDEEMABLE,
ERROR_GIFT_NOT_FOUND, ERROR_REDEEMABLE_NOT_FOUND,
} from '../actions/bucket'; } from '../actions/bucket';
import { toBaseUnit } from "../utils"; import { toBaseUnit } from "../utils";
import { import {
@ -25,11 +25,11 @@ const REDEEM_CODE = "hello world";
const buckerErrorMessage = (error: BucketErrors): string => { const buckerErrorMessage = (error: BucketErrors): string => {
switch (error.type) { switch (error.type) {
case ERROR_LOADING_GIFT: case ERROR_LOADING_REDEEMABLE:
return "couldn't load gift"; return "couldn't load redeemable";
case ERROR_GIFT_NOT_FOUND: case ERROR_REDEEMABLE_NOT_FOUND:
return "gift not found or already redeemed"; return "redeemable not found or already redeemed";
default: default:
return "something went wrong"; return "something went wrong";
@ -79,7 +79,7 @@ export default function(ownProps: any) {
}, shallowEqual); }, shallowEqual);
useEffect(() => { useEffect(() => {
dispatch(loadGift(bucketAddress, recipientAddress)); dispatch(loadRedeemable(bucketAddress, recipientAddress));
}, [bucketAddress, recipientAddress]); }, [bucketAddress, recipientAddress]);
if (props.error) { if (props.error) {

View File

@ -1,9 +1,9 @@
import { import {
BucketActions, BucketActions,
BucketErrors, BucketErrors,
BUCKET_GIFT_LOADING, BUCKET_REDEEMABLE_LOADING,
BUCKET_GIFT_NOT_FOUND, BUCKET_REDEEMABLE_NOT_FOUND,
BUCKET_GIFT_LOADED, BUCKET_REDEEMABLE_LOADED,
BUCKET_TOKEN_LOADING, BUCKET_TOKEN_LOADING,
BUCKET_TOKEN_LOADED, BUCKET_TOKEN_LOADED,
} from "../actions/bucket"; } from "../actions/bucket";
@ -36,7 +36,7 @@ const initialState: BucketState = {
export const bucketReducer = (state: BucketState = initialState, action: BucketActions): BucketState => { export const bucketReducer = (state: BucketState = initialState, action: BucketActions): BucketState => {
switch (action.type) { switch (action.type) {
case BUCKET_GIFT_LOADING: { case BUCKET_REDEEMABLE_LOADING: {
return { return {
...initialState, ...initialState,
loading: true, loading: true,
@ -45,7 +45,7 @@ export const bucketReducer = (state: BucketState = initialState, action: BucketA
} }
} }
case BUCKET_GIFT_NOT_FOUND: { case BUCKET_REDEEMABLE_NOT_FOUND: {
return { return {
...state, ...state,
loading: false, loading: false,
@ -53,7 +53,7 @@ export const bucketReducer = (state: BucketState = initialState, action: BucketA
} }
} }
case BUCKET_GIFT_LOADED: { case BUCKET_REDEEMABLE_LOADED: {
return { return {
...state, ...state,
loading: false, loading: false,

View File

@ -6,8 +6,8 @@ import {
REDEEM_DONE, REDEEM_DONE,
} from "../actions/redeem"; } from "../actions/redeem";
import { import {
BucketGiftLoadingAction, BucketRedeemableLoadingAction,
BUCKET_GIFT_LOADING BUCKET_REDEEMABLE_LOADING
} from "../actions/bucket"; } from "../actions/bucket";
export interface RedeemState { export interface RedeemState {
@ -24,9 +24,9 @@ const initialState: RedeemState = {
receiver: undefined, receiver: undefined,
} }
export const redeemReducer = (state: RedeemState = initialState, action: RedeemActions | BucketGiftLoadingAction): RedeemState => { export const redeemReducer = (state: RedeemState = initialState, action: RedeemActions | BucketRedeemableLoadingAction): RedeemState => {
switch (action.type) { switch (action.type) {
case BUCKET_GIFT_LOADING: { case BUCKET_REDEEMABLE_LOADING: {
return initialState; return initialState;
} }

View File

@ -33,7 +33,7 @@ module.exports = {
TestToken: { TestToken: {
args: ["TEST", 18], args: ["TEST", 18],
}, },
GiftBucketFactory: { ERC20BucketFactory: {
params: [], params: [],
}, },
} }

View File

@ -13,7 +13,7 @@ abstract contract Bucket {
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
bytes32 DOMAIN_SEPARATOR; bytes32 DOMAIN_SEPARATOR;
struct Gift { struct Redeemable {
address recipient; address recipient;
bytes32 code; bytes32 code;
uint256 data; uint256 data;
@ -26,7 +26,7 @@ abstract contract Bucket {
bytes32 code; bytes32 code;
} }
mapping(address => Gift) public gifts; mapping(address => Redeemable) public redeemables;
modifier onlyOwner() { modifier onlyOwner() {
require(msg.sender == owner, "owner required"); require(msg.sender == owner, "owner required");
@ -67,16 +67,16 @@ abstract contract Bucket {
address recipient = recoverSigner(DOMAIN_SEPARATOR, _redeem, _sig); address recipient = recoverSigner(DOMAIN_SEPARATOR, _redeem, _sig);
Gift storage gift = gifts[recipient]; Redeemable storage redeemable = redeemables[recipient];
require(gift.recipient == recipient, "not found"); require(redeemable.recipient == recipient, "not found");
validateCode(_redeem, gift.code); validateCode(_redeem, redeemable.code);
uint256 data = gift.data; uint256 data = redeemable.data;
gift.recipient = address(0); redeemable.recipient = address(0);
gift.code = 0; redeemable.code = 0;
gift.data = 0; redeemable.data = 0;
transferRedeemable(data, _redeem); transferRedeemable(data, _redeem);
} }
@ -109,7 +109,7 @@ abstract contract Bucket {
require(_redeem.blockNumber >= (block.number - _maxTxDelayInBlocks), "transaction too old"); require(_redeem.blockNumber >= (block.number - _maxTxDelayInBlocks), "transaction too old");
require(_redeem.blockHash == blockhash(_redeem.blockNumber), "invalid block hash"); require(_redeem.blockHash == blockhash(_redeem.blockNumber), "invalid block hash");
require(block.timestamp < _expirationTime, "expired gift"); require(block.timestamp < _expirationTime, "expired redeemable");
require(block.timestamp > _startTime, "reedeming not yet started"); require(block.timestamp > _startTime, "reedeming not yet started");
} }

View File

@ -4,13 +4,13 @@ pragma experimental ABIEncoderV2;
import "./Bucket.sol"; import "./Bucket.sol";
import "./erc20/IERC20.sol"; import "./erc20/IERC20.sol";
contract GiftBucket is Bucket { contract ERC20Bucket is Bucket {
uint256 public redeemableSupply; uint256 public redeemableSupply;
constructor( constructor(
address _tokenAddress, address _tokenAddress,
uint256 _startTime, uint256 _startTime,
uint256 _expirationTime) Bucket("KeycardGift", _tokenAddress, _startTime, _expirationTime) public {} uint256 _expirationTime) Bucket("KeycardERC20Bucket", _tokenAddress, _startTime, _expirationTime) public {}
function totalSupply() public view returns(uint256) { function totalSupply() public view returns(uint256) {
return IERC20(tokenAddress).balanceOf(address(this)); return IERC20(tokenAddress).balanceOf(address(this));
@ -23,30 +23,30 @@ contract GiftBucket is Bucket {
return _totalSupply - redeemableSupply; return _totalSupply - redeemableSupply;
} }
function createGift(address recipient, uint256 amount, bytes32 code) external onlyOwner { function createRedeemable(address recipient, uint256 amount, bytes32 code) external onlyOwner {
require(amount > 0, "invalid amount"); require(amount > 0, "invalid amount");
uint256 _availableSupply = this.availableSupply(); uint256 _availableSupply = this.availableSupply();
require(_availableSupply >= amount, "low supply"); require(_availableSupply >= amount, "low supply");
Gift storage gift = gifts[recipient]; Redeemable storage redeemable = redeemables[recipient];
require(gift.recipient == address(0), "recipient already used"); require(redeemable.recipient == address(0), "recipient already used");
gift.recipient = recipient; redeemable.recipient = recipient;
gift.code = code; redeemable.code = code;
gift.data = amount; redeemable.data = amount;
require(redeemableSupply + amount > redeemableSupply, "addition overflow"); require(redeemableSupply + amount > redeemableSupply, "addition overflow");
redeemableSupply += amount; redeemableSupply += amount;
} }
function transferRedeemable(uint256 data, Redeem memory redeem) override internal { function transferRedeemable(uint256 data, Redeem memory redeem) internal override {
require(redeemableSupply >= data, "not enough redeemable supply"); require(redeemableSupply >= data, "not enough redeemable supply");
redeemableSupply -= data; redeemableSupply -= data;
IERC20(tokenAddress).transfer(redeem.receiver, data); IERC20(tokenAddress).transfer(redeem.receiver, data);
} }
function transferRedeemablesToOwner() override internal { function transferRedeemablesToOwner() internal override {
bool success = IERC20(tokenAddress).transfer(owner, this.totalSupply()); bool success = IERC20(tokenAddress).transfer(owner, this.totalSupply());
assert(success); assert(success);
} }

View File

@ -0,0 +1,20 @@
pragma solidity ^0.6.1;
import "./ERC20Bucket.sol";
import "./Proxy.sol";
contract ERC20BucketFactory {
ERC20Bucket public ERC20BucketImplementation;
event BucketCreated(address indexed provider, address indexed bucket);
constructor() public {
ERC20BucketImplementation = new ERC20Bucket(address(0), 0, block.timestamp + 1);
}
function create(address _tokenAddress, uint256 _startTime, uint256 _expirationTime) public returns (address) {
address p = address(new Proxy(abi.encodeWithSelector(0x4e9464ed, "KeycardERC20Bucket", _tokenAddress, _startTime, _expirationTime, msg.sender), address(ERC20BucketImplementation)));
emit BucketCreated(msg.sender, p);
return p;
}
}

View File

@ -1,20 +0,0 @@
pragma solidity ^0.6.1;
import "./GiftBucket.sol";
import "./Proxy.sol";
contract GiftBucketFactory {
GiftBucket public GiftBucketImplementation;
event BucketCreated(address indexed gifter, address indexed bucket);
constructor() public {
GiftBucketImplementation = new GiftBucket(address(0), 0, block.timestamp + 1);
}
function create(address _tokenAddress, uint256 _startTime, uint256 _expirationTime) public returns (address) {
address p = address(new Proxy(abi.encodeWithSelector(0x4e9464ed, "KeycardGift", _tokenAddress, _startTime, _expirationTime, msg.sender), address(GiftBucketImplementation)));
emit BucketCreated(msg.sender, p);
return p;
}
}

View File

@ -12,13 +12,13 @@ contract NFTBucket is Bucket, IERC165, IERC721Receiver {
constructor( constructor(
address _tokenAddress, address _tokenAddress,
uint256 _startTime, uint256 _startTime,
uint256 _expirationTime) Bucket("KeycardNFTGift", _tokenAddress, _startTime, _expirationTime) public {} uint256 _expirationTime) Bucket("KeycardNFTBucket", _tokenAddress, _startTime, _expirationTime) public {}
function transferRedeemable(uint256 data, Redeem memory redeem) override internal { function transferRedeemable(uint256 data, Redeem memory redeem) internal override {
IERC721(tokenAddress).safeTransferFrom(address(this), redeem.receiver, data); IERC721(tokenAddress).safeTransferFrom(address(this), redeem.receiver, data);
} }
function transferRedeemablesToOwner() override internal { function transferRedeemablesToOwner() internal override {
IERC721(tokenAddress).setApprovalForAll(owner, true); IERC721(tokenAddress).setApprovalForAll(owner, true);
assert(IERC721(tokenAddress).isApprovedForAll(address(this), owner)); assert(IERC721(tokenAddress).isApprovedForAll(address(this), owner));
} }
@ -29,7 +29,7 @@ contract NFTBucket is Bucket, IERC165, IERC721Receiver {
function onERC721Received(address _operator, address _from, uint256 _tokenID, bytes calldata _data) external override(IERC721Receiver) returns(bytes4) { function onERC721Received(address _operator, address _from, uint256 _tokenID, bytes calldata _data) external override(IERC721Receiver) returns(bytes4) {
require(msg.sender == tokenAddress, "only the NFT contract can call this"); require(msg.sender == tokenAddress, "only the NFT contract can call this");
require((_operator == owner) || (_from == owner), "only the owner can create gifts"); require((_operator == owner) || (_from == owner), "only the owner can create redeemables");
require(_data.length == 52, "invalid data field"); require(_data.length == 52, "invalid data field");
bytes memory d = _data; bytes memory d = _data;
@ -44,12 +44,12 @@ contract NFTBucket is Bucket, IERC165, IERC721Receiver {
address recipient = address(uint160(uint256(tmp))); address recipient = address(uint160(uint256(tmp)));
Gift storage gift = gifts[recipient]; Redeemable storage redeemable = redeemables[recipient];
require(gift.recipient == address(0), "recipient already used"); require(redeemable.recipient == address(0), "recipient already used");
gift.recipient = recipient; redeemable.recipient = recipient;
gift.code = code; redeemable.code = code;
gift.data = _tokenID; redeemable.data = _tokenID;
return _ERC721_RECEIVED; return _ERC721_RECEIVED;
} }

View File

@ -6,14 +6,14 @@ import "./Proxy.sol";
contract NFTBucketFactory { contract NFTBucketFactory {
NFTBucket public NFTBucketImplementation; NFTBucket public NFTBucketImplementation;
event BucketCreated(address indexed gifter, address indexed bucket); event BucketCreated(address indexed provider, address indexed bucket);
constructor() public { constructor() public {
NFTBucketImplementation = new NFTBucket(address(0), 0, block.timestamp + 1); NFTBucketImplementation = new NFTBucket(address(0), 0, block.timestamp + 1);
} }
function create(address _tokenAddress, uint256 _startTime, uint256 _expirationTime) public returns (address) { function create(address _tokenAddress, uint256 _startTime, uint256 _expirationTime) public returns (address) {
address p = address(new Proxy(abi.encodeWithSelector(0x4e9464ed, "KeycardNFTGift", _tokenAddress, _startTime, _expirationTime, msg.sender), address(NFTBucketImplementation))); address p = address(new Proxy(abi.encodeWithSelector(0x4e9464ed, "KeycardNFTBucket", _tokenAddress, _startTime, _expirationTime, msg.sender), address(NFTBucketImplementation)));
emit BucketCreated(msg.sender, p); emit BucketCreated(msg.sender, p);
return p; return p;
} }

View File

@ -8,7 +8,7 @@ const argv = parseArgs(process.argv.slice(2), {boolean: ["nft", "deploy-factory"
const web3 = new Web3(argv["endpoint"]); const web3 = new Web3(argv["endpoint"]);
const classPrefix = argv["nft"] ? "NFT" : "Gift"; const classPrefix = argv["nft"] ? "NFT" : "ERC20";
const BucketConfig = loadEmbarkArtifact(`./embarkArtifacts/contracts/${classPrefix}Bucket.js`); const BucketConfig = loadEmbarkArtifact(`./embarkArtifacts/contracts/${classPrefix}Bucket.js`);
const BucketFactoryConfig = loadEmbarkArtifact(`./embarkArtifacts/contracts/${classPrefix}BucketFactory.js`); const BucketFactoryConfig = loadEmbarkArtifact(`./embarkArtifacts/contracts/${classPrefix}BucketFactory.js`);
@ -76,9 +76,9 @@ async function deployBucket(sender, factory, token, startInDays, validityInDays)
} }
} }
async function createGift(sender, bucket, keycard) { async function createRedeemable(sender, bucket, keycard) {
Bucket.options.address = bucket; Bucket.options.address = bucket;
let methodCall = Bucket.methods.createGift(keycard.keycard, keycard.amount, keycard.code); let methodCall = Bucket.methods.createRedeemable(keycard.keycard, keycard.amount, keycard.code);
try { try {
let receipt = await sendMethod(methodCall, sender, Bucket.options.address); let receipt = await sendMethod(methodCall, sender, Bucket.options.address);
@ -185,7 +185,7 @@ async function run() {
let file = fs.readFileSync(argv["file"], 'utf8'); let file = fs.readFileSync(argv["file"], 'utf8');
keycards = file.split("\n").map(processLine); keycards = file.split("\n").map(processLine);
for (let keycard of keycards) { for (let keycard of keycards) {
await argv["nft"] ? createGift(sender, bucket, keycard) : transferNFT(sender, argv["token"], bucket, keycard); await argv["nft"] ? createRedeemable(sender, bucket, keycard) : transferNFT(sender, argv["token"], bucket, keycard);
} }
} else if (!hasDoneSomething) { } else if (!hasDoneSomething) {
console.error("the --file option must be specified"); console.error("the --file option must be specified");

View File

@ -1,7 +1,7 @@
const EmbarkJS = artifacts.require('EmbarkJS'); const EmbarkJS = artifacts.require('EmbarkJS');
const TestToken = artifacts.require('TestToken'); const TestToken = artifacts.require('TestToken');
const _GiftBucket = artifacts.require('GiftBucket'); const _ERC20Bucket = artifacts.require('ERC20Bucket');
const GiftBucketFactory = artifacts.require('GiftBucketFactory'); const ERC20BucketFactory = artifacts.require('ERC20BucketFactory');
const TOTAL_SUPPLY = 10000; const TOTAL_SUPPLY = 10000;
const GIFT_AMOUNT = 10; const GIFT_AMOUNT = 10;
@ -22,10 +22,10 @@ config({
"TestToken": { "TestToken": {
args: ["TEST", 18], args: ["TEST", 18],
}, },
"GiftBucket": { "ERC20Bucket": {
args: ["$TestToken", START_TIME, EXPIRATION_TIME], args: ["$TestToken", START_TIME, EXPIRATION_TIME],
}, },
"GiftBucketFactory": { "ERC20BucketFactory": {
args: [], args: [],
}, },
} }
@ -61,7 +61,7 @@ async function signRedeem(contractAddress, signer, message) {
]; ];
const domainData = { const domainData = {
name: "KeycardGift", name: "KeycardERC20Bucket",
version: "1", version: "1",
chainId: chainId, chainId: chainId,
verifyingContract: contractAddress verifyingContract: contractAddress
@ -115,14 +115,14 @@ if (assert.match === undefined) {
} }
} }
contract("GiftBucket", function () { contract("ERC20Bucket", function () {
let GiftBucket; let ERC20Bucket;
sendMethod = (web3.currentProvider.sendAsync) ? web3.currentProvider.sendAsync.bind(web3.currentProvider) : web3.currentProvider.send.bind(web3.currentProvider); sendMethod = (web3.currentProvider.sendAsync) ? web3.currentProvider.sendAsync.bind(web3.currentProvider) : web3.currentProvider.send.bind(web3.currentProvider);
it("deploy factory", async () => { it("deploy factory", async () => {
// only to test gas // only to test gas
const deploy = GiftBucketFactory.deploy({ const deploy = ERC20BucketFactory.deploy({
arguments: [] arguments: []
}); });
@ -132,7 +132,7 @@ contract("GiftBucket", function () {
it("deploy bucket", async () => { it("deploy bucket", async () => {
// only to test gas // only to test gas
const deploy = _GiftBucket.deploy({ const deploy = _ERC20Bucket.deploy({
arguments: [TestToken._address, START_TIME, EXPIRATION_TIME] arguments: [TestToken._address, START_TIME, EXPIRATION_TIME]
}); });
@ -141,7 +141,7 @@ contract("GiftBucket", function () {
}); });
it("deploy bucket via factory", async () => { it("deploy bucket via factory", async () => {
const create = GiftBucketFactory.methods.create(TestToken._address, START_TIME, EXPIRATION_TIME); const create = ERC20BucketFactory.methods.create(TestToken._address, START_TIME, EXPIRATION_TIME);
const gas = await create.estimateGas(); const gas = await create.estimateGas();
const receipt = await create.send({ const receipt = await create.send({
from: shop, from: shop,
@ -149,8 +149,8 @@ contract("GiftBucket", function () {
}); });
const bucketAddress = receipt.events.BucketCreated.returnValues.bucket; const bucketAddress = receipt.events.BucketCreated.returnValues.bucket;
const jsonInterface = _GiftBucket.options.jsonInterface; const jsonInterface = _ERC20Bucket.options.jsonInterface;
GiftBucket = new EmbarkJS.Blockchain.Contract({ ERC20Bucket = new EmbarkJS.Blockchain.Contract({
abi: jsonInterface, abi: jsonInterface,
address: bucketAddress, address: bucketAddress,
}); });
@ -172,99 +172,99 @@ contract("GiftBucket", function () {
}); });
it("add supply", async function() { it("add supply", async function() {
let bucketBalance = await TestToken.methods.balanceOf(GiftBucket._address).call(); let bucketBalance = await TestToken.methods.balanceOf(ERC20Bucket._address).call();
assert.equal(parseInt(bucketBalance), 0, `bucket balance before is ${bucketBalance} instead of 0`); assert.equal(parseInt(bucketBalance), 0, `bucket balance before is ${bucketBalance} instead of 0`);
let shopBalance = await TestToken.methods.balanceOf(shop).call(); let shopBalance = await TestToken.methods.balanceOf(shop).call();
assert.equal(parseInt(shopBalance), TOTAL_SUPPLY, `shop balance before is ${shopBalance} instead of ${TOTAL_SUPPLY}`); assert.equal(parseInt(shopBalance), TOTAL_SUPPLY, `shop balance before is ${shopBalance} instead of ${TOTAL_SUPPLY}`);
const transfer = TestToken.methods.transfer(GiftBucket._address, TOTAL_SUPPLY); const transfer = TestToken.methods.transfer(ERC20Bucket._address, TOTAL_SUPPLY);
const transferGas = await transfer.estimateGas(); const transferGas = await transfer.estimateGas();
await transfer.send({ await transfer.send({
from: shop, from: shop,
gas: transferGas, gas: transferGas,
}); });
bucketBalance = await TestToken.methods.balanceOf(GiftBucket._address).call(); bucketBalance = await TestToken.methods.balanceOf(ERC20Bucket._address).call();
assert.equal(parseInt(bucketBalance), TOTAL_SUPPLY, `bucket balance after is ${bucketBalance} instead of ${TOTAL_SUPPLY}`); assert.equal(parseInt(bucketBalance), TOTAL_SUPPLY, `bucket balance after is ${bucketBalance} instead of ${TOTAL_SUPPLY}`);
shopBalance = await TestToken.methods.balanceOf(shop).call(); shopBalance = await TestToken.methods.balanceOf(shop).call();
assert.equal(parseInt(shopBalance), 0, `shop balance after is ${shopBalance} instead of 0`); assert.equal(parseInt(shopBalance), 0, `shop balance after is ${shopBalance} instead of 0`);
let totalSupply = await GiftBucket.methods.totalSupply().call(); let totalSupply = await ERC20Bucket.methods.totalSupply().call();
assert.equal(parseInt(totalSupply), TOTAL_SUPPLY, `total contract supply is ${totalSupply} instead of ${TOTAL_SUPPLY}`); assert.equal(parseInt(totalSupply), TOTAL_SUPPLY, `total contract supply is ${totalSupply} instead of ${TOTAL_SUPPLY}`);
let availableSupply = await GiftBucket.methods.availableSupply().call(); let availableSupply = await ERC20Bucket.methods.availableSupply().call();
assert.equal(parseInt(availableSupply), TOTAL_SUPPLY, `available contract supply is ${availableSupply} instead of ${TOTAL_SUPPLY}`); assert.equal(parseInt(availableSupply), TOTAL_SUPPLY, `available contract supply is ${availableSupply} instead of ${TOTAL_SUPPLY}`);
}); });
async function testCreateGift(keycard, amount) { async function testCreateRedeemable(keycard, amount) {
let initialSupply = await GiftBucket.methods.totalSupply().call(); let initialSupply = await ERC20Bucket.methods.totalSupply().call();
let initialAvailableSupply = await GiftBucket.methods.availableSupply().call(); let initialAvailableSupply = await ERC20Bucket.methods.availableSupply().call();
const redeemCodeHash = web3.utils.sha3(REDEEM_CODE); const redeemCodeHash = web3.utils.sha3(REDEEM_CODE);
const createGift = GiftBucket.methods.createGift(keycard, amount, redeemCodeHash); const createRedeemable = ERC20Bucket.methods.createRedeemable(keycard, amount, redeemCodeHash);
const createGiftGas = await createGift.estimateGas(); const createRedeemableGas = await createRedeemable.estimateGas();
await createGift.send({ await createRedeemable.send({
from: shop, from: shop,
gas: createGiftGas, gas: createRedeemableGas,
}); });
let totalSupply = await GiftBucket.methods.totalSupply().call(); let totalSupply = await ERC20Bucket.methods.totalSupply().call();
assert.equal(parseInt(totalSupply), parseInt(initialSupply), `totalSupply is ${totalSupply} instead of ${initialSupply}`); assert.equal(parseInt(totalSupply), parseInt(initialSupply), `totalSupply is ${totalSupply} instead of ${initialSupply}`);
let availableSupply = await GiftBucket.methods.availableSupply().call(); let availableSupply = await ERC20Bucket.methods.availableSupply().call();
assert.equal(parseInt(availableSupply), parseInt(initialAvailableSupply) - amount); assert.equal(parseInt(availableSupply), parseInt(initialAvailableSupply) - amount);
} }
it("createGift should fail if amount is zero", async function() { it("createRedeemable should fail if amount is zero", async function() {
try { try {
await testCreateGift(keycard_1, 0); await testCreateRedeemable(keycard_1, 0);
assert.fail("createGift should have failed"); assert.fail("createRedeemable should have failed");
} catch(e) { } catch(e) {
assert.match(e.message, /invalid amount/); assert.match(e.message, /invalid amount/);
} }
}); });
it("createGift fails if amount > totalSupply", async function() { it("createRedeemable fails if amount > totalSupply", async function() {
try { try {
await testCreateGift(keycard_1, TOTAL_SUPPLY + 1); await testCreateRedeemable(keycard_1, TOTAL_SUPPLY + 1);
assert.fail("createGift should have failed"); assert.fail("createRedeemable should have failed");
} catch(e) { } catch(e) {
assert.match(e.message, /low supply/); assert.match(e.message, /low supply/);
} }
}); });
it("createGift", async function() { it("createRedeemable", async function() {
await testCreateGift(keycard_1, GIFT_AMOUNT); await testCreateRedeemable(keycard_1, GIFT_AMOUNT);
}); });
it("createGift should fail if keycard has already been used", async function() { it("createRedeemable should fail if keycard has already been used", async function() {
try { try {
await testCreateGift(keycard_1, 1); await testCreateRedeemable(keycard_1, 1);
assert.fail("createGift should have failed"); assert.fail("createRedeemable should have failed");
} catch(e) { } catch(e) {
assert.match(e.message, /recipient already used/); assert.match(e.message, /recipient already used/);
} }
}); });
it("createGift amount > availableSupply", async function() { it("createRedeemable amount > availableSupply", async function() {
try { try {
await testCreateGift(keycard_2, TOTAL_SUPPLY - GIFT_AMOUNT + 1); await testCreateRedeemable(keycard_2, TOTAL_SUPPLY - GIFT_AMOUNT + 1);
assert.fail("createGift should have failed"); assert.fail("createRedeemable should have failed");
} catch(e) { } catch(e) {
assert.match(e.message, /low supply/); assert.match(e.message, /low supply/);
} }
}); });
async function testRedeem(receiver, recipient, signer, relayer, redeemCode, blockNumber, blockHash) { async function testRedeem(receiver, recipient, signer, relayer, redeemCode, blockNumber, blockHash) {
let initialBucketBalance = await TestToken.methods.balanceOf(GiftBucket._address).call(); let initialBucketBalance = await TestToken.methods.balanceOf(ERC20Bucket._address).call();
let initialUserBalance = await TestToken.methods.balanceOf(user).call(); let initialUserBalance = await TestToken.methods.balanceOf(user).call();
let initialRedeemableSupply = await GiftBucket.methods.redeemableSupply().call(); let initialRedeemableSupply = await ERC20Bucket.methods.redeemableSupply().call();
let gift = await GiftBucket.methods.gifts(recipient).call(); let redeemable = await ERC20Bucket.methods.redeemables(recipient).call();
const amount = parseInt(gift.data); const amount = parseInt(redeemable.data);
const message = { const message = {
blockNumber: blockNumber, blockNumber: blockNumber,
@ -273,8 +273,8 @@ contract("GiftBucket", function () {
code: redeemCode, code: redeemCode,
}; };
const sig = await signRedeem(GiftBucket._address, signer, message); const sig = await signRedeem(ERC20Bucket._address, signer, message);
const redeem = GiftBucket.methods.redeem(message, sig); const redeem = ERC20Bucket.methods.redeem(message, sig);
const redeemGas = await redeem.estimateGas(); const redeemGas = await redeem.estimateGas();
await redeem.send({ await redeem.send({
from: relayer, from: relayer,
@ -283,7 +283,7 @@ contract("GiftBucket", function () {
let expectedBucketBalance = parseInt(initialBucketBalance) - amount; let expectedBucketBalance = parseInt(initialBucketBalance) - amount;
let bucketBalance = await TestToken.methods.balanceOf(GiftBucket._address).call(); let bucketBalance = await TestToken.methods.balanceOf(ERC20Bucket._address).call();
assert.equal(parseInt(bucketBalance), expectedBucketBalance, `bucketBalance after redeem should be ${expectedBucketBalance} instead of ${bucketBalance}`); assert.equal(parseInt(bucketBalance), expectedBucketBalance, `bucketBalance after redeem should be ${expectedBucketBalance} instead of ${bucketBalance}`);
let expectedUserBalance = parseInt(initialUserBalance + amount); let expectedUserBalance = parseInt(initialUserBalance + amount);
@ -291,7 +291,7 @@ contract("GiftBucket", function () {
assert.equal(parseInt(userBalance), expectedUserBalance, `user`, `userBalance after redeem should be ${expectedUserBalance} instead of ${userBalance}`); assert.equal(parseInt(userBalance), expectedUserBalance, `user`, `userBalance after redeem should be ${expectedUserBalance} instead of ${userBalance}`);
let expectedRedeemableSupply = initialRedeemableSupply - amount; let expectedRedeemableSupply = initialRedeemableSupply - amount;
let redeemableSupply = await GiftBucket.methods.redeemableSupply().call(); let redeemableSupply = await ERC20Bucket.methods.redeemableSupply().call();
assert.equal(parseInt(redeemableSupply), expectedRedeemableSupply, `redeemableSupply after redeem should be ${expectedRedeemableSupply} instead of ${redeemableSupply}`); assert.equal(parseInt(redeemableSupply), expectedRedeemableSupply, `redeemableSupply after redeem should be ${expectedRedeemableSupply} instead of ${redeemableSupply}`);
} }
@ -382,9 +382,9 @@ contract("GiftBucket", function () {
async function testKill() { async function testKill() {
let initialShopBalance = parseInt(await TestToken.methods.balanceOf(shop).call()); let initialShopBalance = parseInt(await TestToken.methods.balanceOf(shop).call());
let initialBucketBalance = parseInt(await TestToken.methods.balanceOf(GiftBucket._address).call()); let initialBucketBalance = parseInt(await TestToken.methods.balanceOf(ERC20Bucket._address).call());
await GiftBucket.methods.kill().send({ await ERC20Bucket.methods.kill().send({
from: shop, from: shop,
}); });
@ -392,7 +392,7 @@ contract("GiftBucket", function () {
let shopBalance = await TestToken.methods.balanceOf(shop).call(); let shopBalance = await TestToken.methods.balanceOf(shop).call();
assert.equal(parseInt(shopBalance), expectedShopBalance, `shop balance after kill is ${shopBalance} instead of ${expectedShopBalance}`); assert.equal(parseInt(shopBalance), expectedShopBalance, `shop balance after kill is ${shopBalance} instead of ${expectedShopBalance}`);
let bucketBalance = await TestToken.methods.balanceOf(GiftBucket._address).call(); let bucketBalance = await TestToken.methods.balanceOf(ERC20Bucket._address).call();
assert.equal(parseInt(bucketBalance), 0, `bucketBalance after kill is ${bucketBalance} instead of 0`); assert.equal(parseInt(bucketBalance), 0, `bucketBalance after kill is ${bucketBalance} instead of 0`);
} }

View File

@ -62,7 +62,7 @@ async function signRedeem(contractAddress, signer, message) {
]; ];
const domainData = { const domainData = {
name: "KeycardNFTGift", name: "KeycardNFTBucket",
version: "1", version: "1",
chainId: chainId, chainId: chainId,
verifyingContract: contractAddress verifyingContract: contractAddress
@ -157,39 +157,39 @@ contract("NFTBucket", function () {
}); });
}); });
function createGiftData(recipient) { function createRedeemableData(recipient) {
const redeemCodeHash = web3.utils.sha3(REDEEM_CODE); const redeemCodeHash = web3.utils.sha3(REDEEM_CODE);
return recipient + redeemCodeHash.replace("0x", ""); return recipient + redeemCodeHash.replace("0x", "");
} }
async function checkGift(recipient, tokenID) { async function checkRedeemable(recipient, tokenID) {
let gift = await NFTBucket.methods.gifts(recipient).call(); let redeemable = await NFTBucket.methods.redeemables(recipient).call();
assert.equal(gift.recipient, recipient, "gift not found"); assert.equal(redeemable.recipient, recipient, "redeemable not found");
assert.equal(parseInt(gift.data), tokenID, "token ID does not match"); assert.equal(parseInt(redeemable.data), tokenID, "token ID does not match");
let tokenOwner = await TestNFT.methods.ownerOf(tokenID).call(); let tokenOwner = await TestNFT.methods.ownerOf(tokenID).call();
assert.equal(tokenOwner, NFTBucket._address, "token owner is wrong"); assert.equal(tokenOwner, NFTBucket._address, "token owner is wrong");
} }
it("mint directly to gift", async function () { it("mint directly to redeemable", async function () {
await TestNFT.methods.mint(NFTBucket._address, 42, createGiftData(keycard_1)).send({ await TestNFT.methods.mint(NFTBucket._address, 42, createRedeemableData(keycard_1)).send({
from: shop, from: shop,
}); });
await checkGift(keycard_1, 42); await checkRedeemable(keycard_1, 42);
}); });
it("transfer token from shop", async function() { it("transfer token from shop", async function() {
await TestNFT.methods.mint(shop, 0xcafe).send({from: shop,}); await TestNFT.methods.mint(shop, 0xcafe).send({from: shop,});
await TestNFT.methods.safeTransferFrom(shop, NFTBucket._address, 0xcafe, createGiftData(keycard_2)).send({from: shop}); await TestNFT.methods.safeTransferFrom(shop, NFTBucket._address, 0xcafe, createRedeemableData(keycard_2)).send({from: shop});
await checkGift(keycard_2, 0xcafe); await checkRedeemable(keycard_2, 0xcafe);
}); });
it("cannot create two gifts for the same recipient", async function() { it("cannot create two redeemables for the same recipient", async function() {
await TestNFT.methods.mint(shop, 43).send({from: shop}); await TestNFT.methods.mint(shop, 43).send({from: shop});
try { try {
await TestNFT.methods.safeTransferFrom(shop, NFTBucket._address, 43, createGiftData(keycard_2)).send({from: shop}); await TestNFT.methods.safeTransferFrom(shop, NFTBucket._address, 43, createRedeemableData(keycard_2)).send({from: shop});
assert.fail("transfer should have failed"); assert.fail("transfer should have failed");
} catch(e) { } catch(e) {
assert.match(e.message, /already used/); assert.match(e.message, /already used/);
@ -197,9 +197,9 @@ contract("NFTBucket", function () {
}); });
it("cannot create two gifts for the same token", async function() { it("cannot create two redeemables for the same token", async function() {
try { try {
await NFTBucket.methods.onERC721Received(shop, shop, 0xcafe, createGiftData(keycard_3)).send({from: shop}); await NFTBucket.methods.onERC721Received(shop, shop, 0xcafe, createRedeemableData(keycard_3)).send({from: shop});
assert.fail("transfer should have failed"); assert.fail("transfer should have failed");
} catch(e) { } catch(e) {
assert.match(e.message, /only the NFT/); assert.match(e.message, /only the NFT/);
@ -208,8 +208,8 @@ contract("NFTBucket", function () {
}); });
async function testRedeem(receiver, recipient, signer, relayer, redeemCode, blockNumber, blockHash) { async function testRedeem(receiver, recipient, signer, relayer, redeemCode, blockNumber, blockHash) {
let gift = await NFTBucket.methods.gifts(recipient).call(); let redeemable = await NFTBucket.methods.redeemables(recipient).call();
const tokenID = gift.data; const tokenID = redeemable.data;
const message = { const message = {
blockNumber: blockNumber, blockNumber: blockNumber,