mirror of
https://github.com/status-im/keycard-redeem.git
synced 2025-02-21 00:08:30 +00:00
Merge pull request #22 from status-im/gift-to-erc20-redeemable
remove gift terminology
This commit is contained in:
commit
a7e86d8652
@ -1,60 +1,60 @@
|
||||
import { RootState } from '../reducers';
|
||||
import GiftBucket from '../../../embarkArtifacts/contracts/GiftBucket';
|
||||
import ERC20Bucket from '../../../embarkArtifacts/contracts/ERC20Bucket';
|
||||
import IERC20Detailed from '../../../embarkArtifacts/contracts/IERC20Detailed';
|
||||
import { config } from "../config";
|
||||
import { Contract } from 'web3-eth-contract';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
export const ERROR_GIFT_NOT_FOUND = "ERROR_GIFT_NOT_FOUND";
|
||||
export interface ErrGiftNotFound {
|
||||
type: typeof ERROR_GIFT_NOT_FOUND
|
||||
export const ERROR_REDEEMABLE_NOT_FOUND = "ERROR_REDEEMABLE_NOT_FOUND";
|
||||
export interface ErrRedeemableNotFound {
|
||||
type: typeof ERROR_REDEEMABLE_NOT_FOUND
|
||||
}
|
||||
|
||||
export const ERROR_LOADING_GIFT = "ERROR_LOADING_GIFT";
|
||||
export interface ErrLoadingGift {
|
||||
type: typeof ERROR_LOADING_GIFT
|
||||
export const ERROR_LOADING_REDEEMABLE = "ERROR_LOADING_REDEEMABLE";
|
||||
export interface ErrLoadingRedeemable {
|
||||
type: typeof ERROR_LOADING_REDEEMABLE
|
||||
message: string
|
||||
}
|
||||
|
||||
export type BucketErrors =
|
||||
ErrGiftNotFound |
|
||||
ErrLoadingGift;
|
||||
ErrRedeemableNotFound |
|
||||
ErrLoadingRedeemable;
|
||||
|
||||
const errGiftNotFound = (): ErrGiftNotFound => ({
|
||||
type: ERROR_GIFT_NOT_FOUND,
|
||||
const errRedeemableNotFound = (): ErrRedeemableNotFound => ({
|
||||
type: ERROR_REDEEMABLE_NOT_FOUND,
|
||||
});
|
||||
|
||||
const errLoadingGift = (message: string): ErrLoadingGift => ({
|
||||
type: ERROR_LOADING_GIFT,
|
||||
const errLoadingRedeemable = (message: string): ErrLoadingRedeemable => ({
|
||||
type: ERROR_LOADING_REDEEMABLE,
|
||||
message,
|
||||
});
|
||||
|
||||
export const BUCKET_GIFT_LOADING = "BUCKET_GIFT_LOADING";
|
||||
export interface BucketGiftLoadingAction {
|
||||
type: typeof BUCKET_GIFT_LOADING
|
||||
export const BUCKET_REDEEMABLE_LOADING = "BUCKET_REDEEMABLE_LOADING";
|
||||
export interface BucketRedeemableLoadingAction {
|
||||
type: typeof BUCKET_REDEEMABLE_LOADING
|
||||
address: string
|
||||
recipient: string
|
||||
}
|
||||
|
||||
export const BUCKET_GIFT_LOADING_ERROR = "BUCKET_GIFT_LOADING_ERROR";
|
||||
export interface BucketGiftLoadingErrorAction {
|
||||
type: typeof BUCKET_GIFT_LOADING_ERROR
|
||||
error: ErrLoadingGift
|
||||
export const BUCKET_REDEEMABLE_LOADING_ERROR = "BUCKET_REDEEMABLE_LOADING_ERROR";
|
||||
export interface BucketRedeemableLoadingErrorAction {
|
||||
type: typeof BUCKET_REDEEMABLE_LOADING_ERROR
|
||||
error: ErrLoadingRedeemable
|
||||
}
|
||||
|
||||
export const BUCKET_GIFT_LOADED = "BUCKET_GIFT_LOADED";
|
||||
export interface BucketGiftLoadedAction {
|
||||
type: typeof BUCKET_GIFT_LOADED
|
||||
export const BUCKET_REDEEMABLE_LOADED = "BUCKET_REDEEMABLE_LOADED";
|
||||
export interface BucketRedeemableLoadedAction {
|
||||
type: typeof BUCKET_REDEEMABLE_LOADED
|
||||
expirationTime: number
|
||||
recipient: string
|
||||
amount: string
|
||||
codeHash: string
|
||||
}
|
||||
|
||||
export const BUCKET_GIFT_NOT_FOUND = "BUCKET_GIFT_NOT_FOUND";
|
||||
export interface BucketGiftNotFoundAction {
|
||||
type: typeof BUCKET_GIFT_NOT_FOUND
|
||||
error: ErrGiftNotFound
|
||||
export const BUCKET_REDEEMABLE_NOT_FOUND = "BUCKET_REDEEMABLE_NOT_FOUND";
|
||||
export interface BucketRedeemableNotFoundAction {
|
||||
type: typeof BUCKET_REDEEMABLE_NOT_FOUND
|
||||
error: ErrRedeemableNotFound
|
||||
}
|
||||
|
||||
export const BUCKET_TOKEN_LOADING = "BUCKET_TOKEN_LOADING";
|
||||
@ -71,35 +71,35 @@ export interface BucketTokenLoadedAction {
|
||||
}
|
||||
|
||||
export type BucketActions =
|
||||
BucketGiftLoadingAction |
|
||||
BucketGiftLoadingErrorAction |
|
||||
BucketGiftLoadedAction |
|
||||
BucketGiftNotFoundAction |
|
||||
BucketRedeemableLoadingAction |
|
||||
BucketRedeemableLoadingErrorAction |
|
||||
BucketRedeemableLoadedAction |
|
||||
BucketRedeemableNotFoundAction |
|
||||
BucketTokenLoadingAction |
|
||||
BucketTokenLoadedAction;
|
||||
|
||||
export const loadingGift = (address: string, recipient: string): BucketGiftLoadingAction => ({
|
||||
type: BUCKET_GIFT_LOADING,
|
||||
export const loadingRedeemable = (address: string, recipient: string): BucketRedeemableLoadingAction => ({
|
||||
type: BUCKET_REDEEMABLE_LOADING,
|
||||
address,
|
||||
recipient,
|
||||
});
|
||||
|
||||
export const giftLoaded = (expirationTime: number, recipient: string, amount: string, codeHash: string): BucketGiftLoadedAction => ({
|
||||
type: BUCKET_GIFT_LOADED,
|
||||
export const redeemableLoaded = (expirationTime: number, recipient: string, amount: string, codeHash: string): BucketRedeemableLoadedAction => ({
|
||||
type: BUCKET_REDEEMABLE_LOADED,
|
||||
expirationTime,
|
||||
recipient,
|
||||
amount,
|
||||
codeHash,
|
||||
});
|
||||
|
||||
export const giftNotFound = (): BucketGiftNotFoundAction => ({
|
||||
type: BUCKET_GIFT_NOT_FOUND,
|
||||
error: errGiftNotFound(),
|
||||
export const redeemableNotFound = (): BucketRedeemableNotFoundAction => ({
|
||||
type: BUCKET_REDEEMABLE_NOT_FOUND,
|
||||
error: errRedeemableNotFound(),
|
||||
});
|
||||
|
||||
export const errorLoadingGift = (errorMessage: string): BucketGiftLoadingErrorAction => ({
|
||||
type: BUCKET_GIFT_LOADING_ERROR,
|
||||
error: errLoadingGift(errorMessage),
|
||||
export const errorLoadingRedeemable = (errorMessage: string): BucketRedeemableLoadingErrorAction => ({
|
||||
type: BUCKET_REDEEMABLE_LOADING_ERROR,
|
||||
error: errLoadingRedeemable(errorMessage),
|
||||
});
|
||||
|
||||
export const loadingToken = (address: string): BucketTokenLoadingAction => ({
|
||||
@ -114,7 +114,7 @@ export const tokenLoaded = (symbol: string, decimals: number): BucketTokenLoaded
|
||||
});
|
||||
|
||||
export const newBucketContract = (address: string) => {
|
||||
const bucketAbi = GiftBucket.options.jsonInterface;
|
||||
const bucketAbi = ERC20Bucket.options.jsonInterface;
|
||||
const bucket = new config.web3!.eth.Contract(bucketAbi, address);
|
||||
return bucket;
|
||||
}
|
||||
@ -125,22 +125,22 @@ const newERC20Contract = (address: string) => {
|
||||
return erc20;
|
||||
}
|
||||
|
||||
export const loadGift = (bucketAddress: string, recipientAddress: string) => {
|
||||
export const loadRedeemable = (bucketAddress: string, recipientAddress: string) => {
|
||||
return async (dispatch: Dispatch, getState: () => RootState) => {
|
||||
dispatch(loadingGift(bucketAddress, recipientAddress));
|
||||
dispatch(loadingRedeemable(bucketAddress, recipientAddress));
|
||||
const bucket = newBucketContract(bucketAddress);
|
||||
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;
|
||||
if (amount === "0") {
|
||||
dispatch(giftNotFound())
|
||||
dispatch(redeemableNotFound())
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(giftLoaded(expirationTime, recipient, amount, code));
|
||||
dispatch(redeemableLoaded(expirationTime, recipient, amount, code));
|
||||
dispatch<any>(loadToken(bucket))
|
||||
}).catch(err => {
|
||||
dispatch(errorLoadingGift(err))
|
||||
dispatch(errorLoadingRedeemable(err))
|
||||
console.error("err: ", err)
|
||||
})
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { RootState } from '../reducers';
|
||||
import GiftBucket from '../../../embarkArtifacts/contracts/GiftBucket';
|
||||
import ERC20Bucket from '../../../embarkArtifacts/contracts/ERC20Bucket';
|
||||
import IERC20Detailed from '../../../embarkArtifacts/contracts/IERC20Detailed';
|
||||
import { config } from "../config";
|
||||
import { Dispatch } from 'redux';
|
||||
@ -154,7 +154,7 @@ async function signRedeem(web3Type: Web3Type, contractAddress: string, signer: s
|
||||
];
|
||||
|
||||
const domainData = {
|
||||
name: "KeycardGift",
|
||||
name: "KeycardERC20Bucket",
|
||||
version: "1",
|
||||
chainId: chainId,
|
||||
verifyingContract: contractAddress
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import GiftBucketFactory from '../../../embarkArtifacts/contracts/GiftBucketFactory';
|
||||
import ERC20BucketFactory from '../../../embarkArtifacts/contracts/ERC20BucketFactory';
|
||||
import {
|
||||
shallowEqual,
|
||||
useSelector,
|
||||
@ -40,7 +40,7 @@ export default function(ownProps: any) {
|
||||
|
||||
return <>
|
||||
Network ID: {props.networkID} <br />
|
||||
Factory: {GiftBucketFactory.address} <br />
|
||||
Factory: {ERC20BucketFactory.address} <br />
|
||||
Web3 Type: {web3Type(props.type)}
|
||||
<hr />
|
||||
<div>
|
||||
|
@ -8,10 +8,10 @@ import {
|
||||
} from 'react-redux';
|
||||
import { redeemPath } from '../config';
|
||||
import {
|
||||
loadGift,
|
||||
loadRedeemable,
|
||||
BucketErrors,
|
||||
ERROR_LOADING_GIFT,
|
||||
ERROR_GIFT_NOT_FOUND,
|
||||
ERROR_LOADING_REDEEMABLE,
|
||||
ERROR_REDEEMABLE_NOT_FOUND,
|
||||
} from '../actions/bucket';
|
||||
import { toBaseUnit } from "../utils";
|
||||
import {
|
||||
@ -25,11 +25,11 @@ const REDEEM_CODE = "hello world";
|
||||
|
||||
const buckerErrorMessage = (error: BucketErrors): string => {
|
||||
switch (error.type) {
|
||||
case ERROR_LOADING_GIFT:
|
||||
return "couldn't load gift";
|
||||
case ERROR_LOADING_REDEEMABLE:
|
||||
return "couldn't load redeemable";
|
||||
|
||||
case ERROR_GIFT_NOT_FOUND:
|
||||
return "gift not found or already redeemed";
|
||||
case ERROR_REDEEMABLE_NOT_FOUND:
|
||||
return "redeemable not found or already redeemed";
|
||||
|
||||
default:
|
||||
return "something went wrong";
|
||||
@ -79,7 +79,7 @@ export default function(ownProps: any) {
|
||||
}, shallowEqual);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(loadGift(bucketAddress, recipientAddress));
|
||||
dispatch(loadRedeemable(bucketAddress, recipientAddress));
|
||||
}, [bucketAddress, recipientAddress]);
|
||||
|
||||
if (props.error) {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {
|
||||
BucketActions,
|
||||
BucketErrors,
|
||||
BUCKET_GIFT_LOADING,
|
||||
BUCKET_GIFT_NOT_FOUND,
|
||||
BUCKET_GIFT_LOADED,
|
||||
BUCKET_REDEEMABLE_LOADING,
|
||||
BUCKET_REDEEMABLE_NOT_FOUND,
|
||||
BUCKET_REDEEMABLE_LOADED,
|
||||
BUCKET_TOKEN_LOADING,
|
||||
BUCKET_TOKEN_LOADED,
|
||||
} from "../actions/bucket";
|
||||
@ -36,7 +36,7 @@ const initialState: BucketState = {
|
||||
|
||||
export const bucketReducer = (state: BucketState = initialState, action: BucketActions): BucketState => {
|
||||
switch (action.type) {
|
||||
case BUCKET_GIFT_LOADING: {
|
||||
case BUCKET_REDEEMABLE_LOADING: {
|
||||
return {
|
||||
...initialState,
|
||||
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 {
|
||||
...state,
|
||||
loading: false,
|
||||
@ -53,7 +53,7 @@ export const bucketReducer = (state: BucketState = initialState, action: BucketA
|
||||
}
|
||||
}
|
||||
|
||||
case BUCKET_GIFT_LOADED: {
|
||||
case BUCKET_REDEEMABLE_LOADED: {
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
|
@ -6,8 +6,8 @@ import {
|
||||
REDEEM_DONE,
|
||||
} from "../actions/redeem";
|
||||
import {
|
||||
BucketGiftLoadingAction,
|
||||
BUCKET_GIFT_LOADING
|
||||
BucketRedeemableLoadingAction,
|
||||
BUCKET_REDEEMABLE_LOADING
|
||||
} from "../actions/bucket";
|
||||
|
||||
export interface RedeemState {
|
||||
@ -24,9 +24,9 @@ const initialState: RedeemState = {
|
||||
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) {
|
||||
case BUCKET_GIFT_LOADING: {
|
||||
case BUCKET_REDEEMABLE_LOADING: {
|
||||
return initialState;
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ module.exports = {
|
||||
TestToken: {
|
||||
args: ["TEST", 18],
|
||||
},
|
||||
GiftBucketFactory: {
|
||||
ERC20BucketFactory: {
|
||||
params: [],
|
||||
},
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ abstract contract Bucket {
|
||||
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
||||
bytes32 DOMAIN_SEPARATOR;
|
||||
|
||||
struct Gift {
|
||||
struct Redeemable {
|
||||
address recipient;
|
||||
bytes32 code;
|
||||
uint256 data;
|
||||
@ -26,7 +26,7 @@ abstract contract Bucket {
|
||||
bytes32 code;
|
||||
}
|
||||
|
||||
mapping(address => Gift) public gifts;
|
||||
mapping(address => Redeemable) public redeemables;
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner, "owner required");
|
||||
@ -67,16 +67,16 @@ abstract contract Bucket {
|
||||
|
||||
address recipient = recoverSigner(DOMAIN_SEPARATOR, _redeem, _sig);
|
||||
|
||||
Gift storage gift = gifts[recipient];
|
||||
require(gift.recipient == recipient, "not found");
|
||||
Redeemable storage redeemable = redeemables[recipient];
|
||||
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);
|
||||
gift.code = 0;
|
||||
gift.data = 0;
|
||||
redeemable.recipient = address(0);
|
||||
redeemable.code = 0;
|
||||
redeemable.data = 0;
|
||||
|
||||
transferRedeemable(data, _redeem);
|
||||
}
|
||||
@ -109,7 +109,7 @@ abstract contract Bucket {
|
||||
require(_redeem.blockNumber >= (block.number - _maxTxDelayInBlocks), "transaction too old");
|
||||
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");
|
||||
}
|
||||
|
||||
|
@ -4,13 +4,13 @@ pragma experimental ABIEncoderV2;
|
||||
import "./Bucket.sol";
|
||||
import "./erc20/IERC20.sol";
|
||||
|
||||
contract GiftBucket is Bucket {
|
||||
contract ERC20Bucket is Bucket {
|
||||
uint256 public redeemableSupply;
|
||||
|
||||
constructor(
|
||||
address _tokenAddress,
|
||||
uint256 _startTime,
|
||||
uint256 _expirationTime) Bucket("KeycardGift", _tokenAddress, _startTime, _expirationTime) public {}
|
||||
uint256 _expirationTime) Bucket("KeycardERC20Bucket", _tokenAddress, _startTime, _expirationTime) public {}
|
||||
|
||||
function totalSupply() public view returns(uint256) {
|
||||
return IERC20(tokenAddress).balanceOf(address(this));
|
||||
@ -23,30 +23,30 @@ contract GiftBucket is Bucket {
|
||||
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");
|
||||
|
||||
uint256 _availableSupply = this.availableSupply();
|
||||
require(_availableSupply >= amount, "low supply");
|
||||
|
||||
Gift storage gift = gifts[recipient];
|
||||
require(gift.recipient == address(0), "recipient already used");
|
||||
Redeemable storage redeemable = redeemables[recipient];
|
||||
require(redeemable.recipient == address(0), "recipient already used");
|
||||
|
||||
gift.recipient = recipient;
|
||||
gift.code = code;
|
||||
gift.data = amount;
|
||||
redeemable.recipient = recipient;
|
||||
redeemable.code = code;
|
||||
redeemable.data = amount;
|
||||
|
||||
require(redeemableSupply + amount > redeemableSupply, "addition overflow");
|
||||
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");
|
||||
redeemableSupply -= data;
|
||||
IERC20(tokenAddress).transfer(redeem.receiver, data);
|
||||
}
|
||||
|
||||
function transferRedeemablesToOwner() override internal {
|
||||
function transferRedeemablesToOwner() internal override {
|
||||
bool success = IERC20(tokenAddress).transfer(owner, this.totalSupply());
|
||||
assert(success);
|
||||
}
|
20
contracts/ERC20BucketFactory.sol
Normal file
20
contracts/ERC20BucketFactory.sol
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -12,13 +12,13 @@ contract NFTBucket is Bucket, IERC165, IERC721Receiver {
|
||||
constructor(
|
||||
address _tokenAddress,
|
||||
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);
|
||||
}
|
||||
|
||||
function transferRedeemablesToOwner() override internal {
|
||||
function transferRedeemablesToOwner() internal override {
|
||||
IERC721(tokenAddress).setApprovalForAll(owner, true);
|
||||
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) {
|
||||
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");
|
||||
|
||||
bytes memory d = _data;
|
||||
@ -44,12 +44,12 @@ contract NFTBucket is Bucket, IERC165, IERC721Receiver {
|
||||
|
||||
address recipient = address(uint160(uint256(tmp)));
|
||||
|
||||
Gift storage gift = gifts[recipient];
|
||||
require(gift.recipient == address(0), "recipient already used");
|
||||
Redeemable storage redeemable = redeemables[recipient];
|
||||
require(redeemable.recipient == address(0), "recipient already used");
|
||||
|
||||
gift.recipient = recipient;
|
||||
gift.code = code;
|
||||
gift.data = _tokenID;
|
||||
redeemable.recipient = recipient;
|
||||
redeemable.code = code;
|
||||
redeemable.data = _tokenID;
|
||||
|
||||
return _ERC721_RECEIVED;
|
||||
}
|
||||
|
@ -6,14 +6,14 @@ import "./Proxy.sol";
|
||||
contract NFTBucketFactory {
|
||||
NFTBucket public NFTBucketImplementation;
|
||||
|
||||
event BucketCreated(address indexed gifter, address indexed bucket);
|
||||
event BucketCreated(address indexed provider, address indexed bucket);
|
||||
|
||||
constructor() public {
|
||||
NFTBucketImplementation = new NFTBucket(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, "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);
|
||||
return p;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ const argv = parseArgs(process.argv.slice(2), {boolean: ["nft", "deploy-factory"
|
||||
|
||||
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 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;
|
||||
let methodCall = Bucket.methods.createGift(keycard.keycard, keycard.amount, keycard.code);
|
||||
let methodCall = Bucket.methods.createRedeemable(keycard.keycard, keycard.amount, keycard.code);
|
||||
|
||||
try {
|
||||
let receipt = await sendMethod(methodCall, sender, Bucket.options.address);
|
||||
@ -185,7 +185,7 @@ async function run() {
|
||||
let file = fs.readFileSync(argv["file"], 'utf8');
|
||||
keycards = file.split("\n").map(processLine);
|
||||
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) {
|
||||
console.error("the --file option must be specified");
|
@ -1,7 +1,7 @@
|
||||
const EmbarkJS = artifacts.require('EmbarkJS');
|
||||
const TestToken = artifacts.require('TestToken');
|
||||
const _GiftBucket = artifacts.require('GiftBucket');
|
||||
const GiftBucketFactory = artifacts.require('GiftBucketFactory');
|
||||
const _ERC20Bucket = artifacts.require('ERC20Bucket');
|
||||
const ERC20BucketFactory = artifacts.require('ERC20BucketFactory');
|
||||
|
||||
const TOTAL_SUPPLY = 10000;
|
||||
const GIFT_AMOUNT = 10;
|
||||
@ -22,10 +22,10 @@ config({
|
||||
"TestToken": {
|
||||
args: ["TEST", 18],
|
||||
},
|
||||
"GiftBucket": {
|
||||
"ERC20Bucket": {
|
||||
args: ["$TestToken", START_TIME, EXPIRATION_TIME],
|
||||
},
|
||||
"GiftBucketFactory": {
|
||||
"ERC20BucketFactory": {
|
||||
args: [],
|
||||
},
|
||||
}
|
||||
@ -61,7 +61,7 @@ async function signRedeem(contractAddress, signer, message) {
|
||||
];
|
||||
|
||||
const domainData = {
|
||||
name: "KeycardGift",
|
||||
name: "KeycardERC20Bucket",
|
||||
version: "1",
|
||||
chainId: chainId,
|
||||
verifyingContract: contractAddress
|
||||
@ -115,14 +115,14 @@ if (assert.match === undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
contract("GiftBucket", function () {
|
||||
let GiftBucket;
|
||||
contract("ERC20Bucket", function () {
|
||||
let ERC20Bucket;
|
||||
|
||||
sendMethod = (web3.currentProvider.sendAsync) ? web3.currentProvider.sendAsync.bind(web3.currentProvider) : web3.currentProvider.send.bind(web3.currentProvider);
|
||||
|
||||
it("deploy factory", async () => {
|
||||
// only to test gas
|
||||
const deploy = GiftBucketFactory.deploy({
|
||||
const deploy = ERC20BucketFactory.deploy({
|
||||
arguments: []
|
||||
});
|
||||
|
||||
@ -132,7 +132,7 @@ contract("GiftBucket", function () {
|
||||
|
||||
it("deploy bucket", async () => {
|
||||
// only to test gas
|
||||
const deploy = _GiftBucket.deploy({
|
||||
const deploy = _ERC20Bucket.deploy({
|
||||
arguments: [TestToken._address, START_TIME, EXPIRATION_TIME]
|
||||
});
|
||||
|
||||
@ -141,7 +141,7 @@ contract("GiftBucket", function () {
|
||||
});
|
||||
|
||||
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 receipt = await create.send({
|
||||
from: shop,
|
||||
@ -149,8 +149,8 @@ contract("GiftBucket", function () {
|
||||
});
|
||||
|
||||
const bucketAddress = receipt.events.BucketCreated.returnValues.bucket;
|
||||
const jsonInterface = _GiftBucket.options.jsonInterface;
|
||||
GiftBucket = new EmbarkJS.Blockchain.Contract({
|
||||
const jsonInterface = _ERC20Bucket.options.jsonInterface;
|
||||
ERC20Bucket = new EmbarkJS.Blockchain.Contract({
|
||||
abi: jsonInterface,
|
||||
address: bucketAddress,
|
||||
});
|
||||
@ -172,99 +172,99 @@ contract("GiftBucket", 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`);
|
||||
|
||||
let shopBalance = await TestToken.methods.balanceOf(shop).call();
|
||||
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();
|
||||
await transfer.send({
|
||||
from: shop,
|
||||
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}`);
|
||||
|
||||
shopBalance = await TestToken.methods.balanceOf(shop).call();
|
||||
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}`);
|
||||
|
||||
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}`);
|
||||
});
|
||||
|
||||
async function testCreateGift(keycard, amount) {
|
||||
let initialSupply = await GiftBucket.methods.totalSupply().call();
|
||||
let initialAvailableSupply = await GiftBucket.methods.availableSupply().call();
|
||||
async function testCreateRedeemable(keycard, amount) {
|
||||
let initialSupply = await ERC20Bucket.methods.totalSupply().call();
|
||||
let initialAvailableSupply = await ERC20Bucket.methods.availableSupply().call();
|
||||
|
||||
const redeemCodeHash = web3.utils.sha3(REDEEM_CODE);
|
||||
const createGift = GiftBucket.methods.createGift(keycard, amount, redeemCodeHash);
|
||||
const createGiftGas = await createGift.estimateGas();
|
||||
await createGift.send({
|
||||
const createRedeemable = ERC20Bucket.methods.createRedeemable(keycard, amount, redeemCodeHash);
|
||||
const createRedeemableGas = await createRedeemable.estimateGas();
|
||||
await createRedeemable.send({
|
||||
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}`);
|
||||
|
||||
let availableSupply = await GiftBucket.methods.availableSupply().call();
|
||||
let availableSupply = await ERC20Bucket.methods.availableSupply().call();
|
||||
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 {
|
||||
await testCreateGift(keycard_1, 0);
|
||||
assert.fail("createGift should have failed");
|
||||
await testCreateRedeemable(keycard_1, 0);
|
||||
assert.fail("createRedeemable should have failed");
|
||||
} catch(e) {
|
||||
assert.match(e.message, /invalid amount/);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it("createGift fails if amount > totalSupply", async function() {
|
||||
it("createRedeemable fails if amount > totalSupply", async function() {
|
||||
try {
|
||||
await testCreateGift(keycard_1, TOTAL_SUPPLY + 1);
|
||||
assert.fail("createGift should have failed");
|
||||
await testCreateRedeemable(keycard_1, TOTAL_SUPPLY + 1);
|
||||
assert.fail("createRedeemable should have failed");
|
||||
} catch(e) {
|
||||
assert.match(e.message, /low supply/);
|
||||
}
|
||||
});
|
||||
|
||||
it("createGift", async function() {
|
||||
await testCreateGift(keycard_1, GIFT_AMOUNT);
|
||||
it("createRedeemable", async function() {
|
||||
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 {
|
||||
await testCreateGift(keycard_1, 1);
|
||||
assert.fail("createGift should have failed");
|
||||
await testCreateRedeemable(keycard_1, 1);
|
||||
assert.fail("createRedeemable should have failed");
|
||||
} catch(e) {
|
||||
assert.match(e.message, /recipient already used/);
|
||||
}
|
||||
});
|
||||
|
||||
it("createGift amount > availableSupply", async function() {
|
||||
it("createRedeemable amount > availableSupply", async function() {
|
||||
try {
|
||||
await testCreateGift(keycard_2, TOTAL_SUPPLY - GIFT_AMOUNT + 1);
|
||||
assert.fail("createGift should have failed");
|
||||
await testCreateRedeemable(keycard_2, TOTAL_SUPPLY - GIFT_AMOUNT + 1);
|
||||
assert.fail("createRedeemable should have failed");
|
||||
} catch(e) {
|
||||
assert.match(e.message, /low supply/);
|
||||
}
|
||||
});
|
||||
|
||||
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 initialRedeemableSupply = await GiftBucket.methods.redeemableSupply().call();
|
||||
let initialRedeemableSupply = await ERC20Bucket.methods.redeemableSupply().call();
|
||||
|
||||
let gift = await GiftBucket.methods.gifts(recipient).call();
|
||||
const amount = parseInt(gift.data);
|
||||
let redeemable = await ERC20Bucket.methods.redeemables(recipient).call();
|
||||
const amount = parseInt(redeemable.data);
|
||||
|
||||
const message = {
|
||||
blockNumber: blockNumber,
|
||||
@ -273,8 +273,8 @@ contract("GiftBucket", function () {
|
||||
code: redeemCode,
|
||||
};
|
||||
|
||||
const sig = await signRedeem(GiftBucket._address, signer, message);
|
||||
const redeem = GiftBucket.methods.redeem(message, sig);
|
||||
const sig = await signRedeem(ERC20Bucket._address, signer, message);
|
||||
const redeem = ERC20Bucket.methods.redeem(message, sig);
|
||||
const redeemGas = await redeem.estimateGas();
|
||||
await redeem.send({
|
||||
from: relayer,
|
||||
@ -283,7 +283,7 @@ contract("GiftBucket", function () {
|
||||
|
||||
|
||||
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}`);
|
||||
|
||||
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}`);
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
@ -382,9 +382,9 @@ contract("GiftBucket", function () {
|
||||
|
||||
async function testKill() {
|
||||
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,
|
||||
});
|
||||
|
||||
@ -392,7 +392,7 @@ contract("GiftBucket", function () {
|
||||
let shopBalance = await TestToken.methods.balanceOf(shop).call();
|
||||
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`);
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ async function signRedeem(contractAddress, signer, message) {
|
||||
];
|
||||
|
||||
const domainData = {
|
||||
name: "KeycardNFTGift",
|
||||
name: "KeycardNFTBucket",
|
||||
version: "1",
|
||||
chainId: chainId,
|
||||
verifyingContract: contractAddress
|
||||
@ -157,39 +157,39 @@ contract("NFTBucket", function () {
|
||||
});
|
||||
});
|
||||
|
||||
function createGiftData(recipient) {
|
||||
function createRedeemableData(recipient) {
|
||||
const redeemCodeHash = web3.utils.sha3(REDEEM_CODE);
|
||||
return recipient + redeemCodeHash.replace("0x", "");
|
||||
}
|
||||
|
||||
async function checkGift(recipient, tokenID) {
|
||||
let gift = await NFTBucket.methods.gifts(recipient).call();
|
||||
assert.equal(gift.recipient, recipient, "gift not found");
|
||||
assert.equal(parseInt(gift.data), tokenID, "token ID does not match");
|
||||
async function checkRedeemable(recipient, tokenID) {
|
||||
let redeemable = await NFTBucket.methods.redeemables(recipient).call();
|
||||
assert.equal(redeemable.recipient, recipient, "redeemable not found");
|
||||
assert.equal(parseInt(redeemable.data), tokenID, "token ID does not match");
|
||||
let tokenOwner = await TestNFT.methods.ownerOf(tokenID).call();
|
||||
assert.equal(tokenOwner, NFTBucket._address, "token owner is wrong");
|
||||
}
|
||||
|
||||
it("mint directly to gift", async function () {
|
||||
await TestNFT.methods.mint(NFTBucket._address, 42, createGiftData(keycard_1)).send({
|
||||
it("mint directly to redeemable", async function () {
|
||||
await TestNFT.methods.mint(NFTBucket._address, 42, createRedeemableData(keycard_1)).send({
|
||||
from: shop,
|
||||
});
|
||||
|
||||
await checkGift(keycard_1, 42);
|
||||
await checkRedeemable(keycard_1, 42);
|
||||
});
|
||||
|
||||
it("transfer token from shop", async function() {
|
||||
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});
|
||||
|
||||
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");
|
||||
} catch(e) {
|
||||
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 {
|
||||
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");
|
||||
} catch(e) {
|
||||
assert.match(e.message, /only the NFT/);
|
||||
@ -208,8 +208,8 @@ contract("NFTBucket", function () {
|
||||
});
|
||||
|
||||
async function testRedeem(receiver, recipient, signer, relayer, redeemCode, blockNumber, blockHash) {
|
||||
let gift = await NFTBucket.methods.gifts(recipient).call();
|
||||
const tokenID = gift.data;
|
||||
let redeemable = await NFTBucket.methods.redeemables(recipient).call();
|
||||
const tokenID = redeemable.data;
|
||||
|
||||
const message = {
|
||||
blockNumber: blockNumber,
|
||||
|
Loading…
x
Reference in New Issue
Block a user