add redeem actions

This commit is contained in:
Andrea Franz 2020-04-01 18:27:06 +02:00
parent 3fbbffca65
commit 5d9e3bcb6c
No known key found for this signature in database
GPG Key ID: 4F0D2F2D9DE7F29D
12 changed files with 394 additions and 34 deletions

View File

@ -16,14 +16,15 @@ export interface ErrLoadingGift {
message: string
}
export type BucketError =
ErrGiftNotFound;
export type BucketErrors =
ErrGiftNotFound |
ErrLoadingGift;
const errGiftNotFound = () => ({
const errGiftNotFound = (): ErrGiftNotFound => ({
type: ERROR_GIFT_NOT_FOUND,
});
const errLoadingGift = (message: string) => ({
const errLoadingGift = (message: string): ErrLoadingGift => ({
type: ERROR_LOADING_GIFT,
message,
});
@ -32,17 +33,19 @@ export const BUCKET_GIFT_LOADING = "BUCKET_GIFT_LOADING";
export interface BucketGiftLoadingAction {
type: typeof BUCKET_GIFT_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: BucketError
error: ErrLoadingGift
}
export const BUCKET_GIFT_LOADED = "BUCKET_GIFT_LOADED";
export interface BucketGiftLoadedAction {
type: typeof BUCKET_GIFT_LOADED
expirationTime: number
recipient: string
amount: string
codeHash: string
@ -51,7 +54,7 @@ export interface BucketGiftLoadedAction {
export const BUCKET_GIFT_NOT_FOUND = "BUCKET_GIFT_NOT_FOUND";
export interface BucketGiftNotFoundAction {
type: typeof BUCKET_GIFT_NOT_FOUND
error: BucketError
error: ErrGiftNotFound
}
export const BUCKET_TOKEN_LOADING = "BUCKET_TOKEN_LOADING";
@ -64,7 +67,7 @@ export const BUCKET_TOKEN_LOADED = "BUCKET_TOKEN_LOADED";
export interface BucketTokenLoadedAction {
type: typeof BUCKET_TOKEN_LOADED
symbol: string
decimal: number
decimals: number
}
export type BucketActions =
@ -75,25 +78,27 @@ export type BucketActions =
BucketTokenLoadingAction |
BucketTokenLoadedAction;
export const loadingGift = (address: string): BucketLoadingAction => ({
export const loadingGift = (address: string, recipient: string): BucketGiftLoadingAction => ({
type: BUCKET_GIFT_LOADING,
address,
recipient,
});
export const giftLoaded = (recipient: string, amount: string, codeHash: string): BucketGiftLoadedAction => ({
export const giftLoaded = (expirationTime: number, recipient: string, amount: string, codeHash: string): BucketGiftLoadedAction => ({
type: BUCKET_GIFT_LOADED,
expirationTime,
recipient,
amount,
codeHash,
});
export const giftNotFound = (recipient: string, amount: string, codeHash: string): BucketGiftNotFoundAction => ({
export const giftNotFound = (): BucketGiftNotFoundAction => ({
type: BUCKET_GIFT_NOT_FOUND,
error: errGiftNotFound(),
});
export const errorLoadingGift = (errorMessage: string): BucketGiftNotFoundAction => ({
type: BUCKET_GIFT_NOT_FOUND,
export const errorLoadingGift = (errorMessage: string): BucketGiftLoadingErrorAction => ({
type: BUCKET_GIFT_LOADING_ERROR,
error: errLoadingGift(errorMessage),
});
@ -108,7 +113,7 @@ export const tokenLoaded = (symbol: string, decimals: number): BucketTokenLoaded
decimals,
});
const newBucketContract = (address: string) => {
export const newBucketContract = (address: string) => {
const bucketAbi = GiftBucket.options.jsonInterface;
const bucket = new config.web3!.eth.Contract(bucketAbi, address);
return bucket;
@ -124,16 +129,16 @@ export const loadGift = (bucketAddress: string, recipientAddress: string) => {
return async (dispatch: Dispatch, getState: () => RootState) => {
dispatch(loadingGift(bucketAddress, recipientAddress));
const bucket = newBucketContract(bucketAddress);
bucket.methods.gifts(recipientAddress).call().then((result: Any) => {
const expirationTime = await bucket.methods.expirationTime().call();
bucket.methods.gifts(recipientAddress).call().then((result: any) => {
const { recipient, amount, code } = result;
if (amount === "0") {
dispatch(giftNotFound())
return;
}
dispatch(giftLoaded(recipient, amount, code));
dispatch(loadToken(bucket))
dispatch(giftLoaded(expirationTime, recipient, amount, code));
dispatch<any>(loadToken(bucket))
}).catch(err => {
dispatch(errorLoadingGift(err))
console.error("err: ", err)

206
app/js/actions/redeem.ts Normal file
View File

@ -0,0 +1,206 @@
import { RootState } from '../reducers';
import GiftBucket from '../../../embarkArtifacts/contracts/GiftBucket';
import IERC20Detailed from '../../../embarkArtifacts/contracts/IERC20Detailed';
import { config } from "../config";
import { Dispatch } from 'redux';
import { newBucketContract } from "./bucket";
import { sha3 } from "web3-utils";
import { recoverTypedSignature } from 'eth-sig-util';
import { Web3Type } from "../actions/web3";
const sleep = (ms: number) => {
return new Promise(resolve => {
window.setTimeout(resolve, ms);
});
}
interface RedeemMessage {
receiver: string
code: string
}
export const ERROR_REDEEMING = "ERROR_REDEEMING";
export interface ErrRedeeming {
type: typeof ERROR_REDEEMING
message: string
}
export const ERROR_WRONG_SIGNER = "ERROR_WRONG_SIGNER";
export interface ErrWrongSigner {
type: typeof ERROR_WRONG_SIGNER
expected: string
actual: string
}
export type RedeemErrors =
ErrRedeeming |
ErrWrongSigner;
export const REDEEM_LOADING = "REDEEM_LOADING";
export interface RedeemLoadingAction {
type: typeof REDEEM_LOADING
}
export const REDEEM_ERROR = "REDEEM_ERROR";
export interface RedeemErrorAction {
type: typeof REDEEM_ERROR
error: RedeemErrors
}
export const REDEEM_DONE = "REDEEM_DONE";
export interface RedeemDoneAction {
type: typeof REDEEM_DONE
txHash: string
}
export type RedeemActions =
RedeemLoadingAction |
RedeemErrorAction |
RedeemDoneAction;
const redeeming = () => ({
type: REDEEM_LOADING,
});
const wrongSigner = (expected: string, actual: string) => ({
type: REDEEM_ERROR,
error: {
type: ERROR_WRONG_SIGNER,
expected,
actual,
}
});
const redeemError = (message: string) => ({
type: REDEEM_ERROR,
error: {
type: ERROR_REDEEMING,
message,
}
});
const redeemDone = (txHash: string) => ({
type: REDEEM_DONE,
txHash,
});
export const redeem = (bucketAddress: string, recipientAddress: string, code: string) => {
return (dispatch: Dispatch, getState: () => RootState) => {
dispatch(redeeming());
const state = getState();
const web3Type = state.web3.type;
const bucketAddress = state.bucket.address;
const bucket = newBucketContract(bucketAddress);
const codeHash = sha3(code);
const account = state.web3.account;
const message = {
receiver: state.web3.account,
code: codeHash,
};
//FIXME: is signer needed?
signRedeem(web3Type, bucketAddress, state.web3.account, message).then(async ({ sig, address }: SignRedeemResponse) => {
const recipient = state.bucket.recipient;
//FIXME: remove! hack to wait for the request screen to slide down
await sleep(3000);
if (address.toLowerCase() != recipient.toLowerCase()) {
//FIXME: handle error
dispatch(wrongSigner(recipient, address));
return;
}
const redeem = bucket.methods.redeem(message, sig);
const gas = await redeem.estimateGas();
redeem.send({ from: account, gas }).then(resp => {
dispatch(redeemDone(resp.transactionHash));
}).catch(err => {
console.error("redeem error: ", err);
dispatch(redeemError(err))
});
}).catch(err => {
console.error("sign redeem error: ", err);
dispatch(redeemError(err))
});
}
}
interface SignRedeemResponse {
sig: string
address: string
}
async function signRedeem(web3Type: Web3Type, contractAddress: string, signer: string, message: RedeemMessage): Promise<SignRedeemResponse> {
const chainId = await config.web3!.eth.net.getId();
const domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" }
];
const redeem = [
{ name: "receiver", type: "address" },
{ name: "code", type: "bytes32" },
];
const domainData = {
name: "KeycardGift",
version: "1",
chainId: chainId,
verifyingContract: contractAddress
};
const data = {
types: {
EIP712Domain: domain,
Redeem: redeem,
},
primaryType: ("Redeem" as const),
domain: domainData,
message: message
};
if (web3Type === Web3Type.Status) {
return signWithKeycard(signer, data);
} else {
return signWithWeb3(signer, data);
}
}
const signWithWeb3 = (signer: string, data: any) => {
return new Promise((resolve, reject) => {
(window as any).ethereum.sendAsync({
method: "eth_signTypedData_v3",
params: [signer, JSON.stringify(data)],
from: signer,
}, (err, resp) => {
if (err) {
reject(err);
} else {
const sig = resp.result;
const address = recoverTypedSignature({
data,
sig
});
resolve({ sig, address });
}
})
});
}
const signWithKeycard = (signer: string, data: any) => {
return new Promise((resolve, reject) => {
(window as any).ethereum.send("keycard_signTypedData", [signer, JSON.stringify(data)]).then(resp => {
const sig = resp.result;
const address = recoverTypedSignature({
data,
sig
});
resolve({ sig, address });
}).catch(err => {
reject(err);
})
});
}

View File

@ -11,10 +11,11 @@ export const VALID_NETWORK_ID = 3;
// export const VALID_NETWORK_ID = 5;
export const LOCAL_NETWORK_ID = 1337;
enum Web3Type {
export enum Web3Type {
None,
Generic,
Remote,
Status,
Status
}
export const WEB3_INITIALIZED = "WEB3_INITIALIZED";

View File

@ -1,9 +1,24 @@
import React from 'react';
import GiftBucketFactory from '../../../embarkArtifacts/contracts/GiftBucketFactory';
import {
shallowEqual,
useSelector,
useDispatch,
} from 'react-redux';
import { Web3Type } from "../actions/web3";
const web3Type = (t: Web3Type) => {
switch (t) {
case Web3Type.None:
return "not a web3 browser";
case Web3Type.Generic:
return "generic web3 browser";
case Web3Type.Remote:
return "remote web3 node";
case Web3Type.Status:
return "status web3 browser";
}
}
export default function(ownProps: any) {
const props = useSelector(state => {
@ -11,6 +26,7 @@ export default function(ownProps: any) {
initialized: state.web3.networkID,
networkID: state.web3.networkID,
error: state.web3.error,
type: state.web3.type,
}
}, shallowEqual);
@ -22,5 +38,13 @@ export default function(ownProps: any) {
return "initializing...";
}
return ownProps.children;
return <>
Network ID: {props.networkID} <br />
Factory: {GiftBucketFactory.address} <br />
Web3 Type: {web3Type(props.type)}
<hr />
<div>
{ownProps.children}
</div>
</>;
}

View File

@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
import { RootState } from '../reducers';
import { useRouteMatch } from 'react-router-dom';
import {
shallowEqual,
@ -8,19 +9,40 @@ import {
import { redeemPath } from '../config';
import {
loadGift,
BucketError,
BucketErrors,
ERROR_LOADING_GIFT,
ERROR_GIFT_NOT_FOUND,
} from '../actions/bucket';
import { toBaseUnit } from "../utils";
import {
redeem,
RedeemErrors,
ERROR_REDEEMING,
ERROR_WRONG_SIGNER,
} from '../actions/redeem';
const errorMessage = (error: BucketError): string => {
const REDEEM_CODE = "hello world";
const buckerErrorMessage = (error: BucketErrors): string => {
switch (error.type) {
case ERROR_LOADING_GIFT:
return "couldn't load gift.";
return "couldn't load gift";
case ERROR_GIFT_NOT_FOUND:
return "gift not found";
return "gift not found or already redeemed";
default:
return "something went wrong";
}
}
const redeemErrorMessage = (error: RedeemErrors): string => {
switch (error.type) {
case ERROR_WRONG_SIGNER:
return `wrong signer. expected signature from ${error.expected}, got signature from ${error.actual}`;
case ERROR_REDEEMING:
return `redeem error: ${error.message}`;
default:
return "something went wrong";
@ -37,11 +59,11 @@ export default function(ownProps: any) {
const bucketAddress = match.params.bucketAddress;
const recipientAddress = match.params.recipientAddress;
const props = useSelector(state => {
const props = useSelector((state: RootState) => {
return {
bucketAddress: state.bucket.address,
loading: state.bucket.loading,
found: state.bucket.found,
expirationTime: state.bucket.expirationTime,
error: state.bucket.error,
recipient: state.bucket.recipient,
amount: state.bucket.amount,
@ -50,6 +72,9 @@ export default function(ownProps: any) {
tokenSymbol: state.bucket.tokenSymbol,
tokenDecimals: state.bucket.tokenDecimals,
receiver: state.web3.account,
redeeming: state.redeem.loading,
redeemError: state.redeem.error,
redeemTxHash: state.redeem.txHash,
}
}, shallowEqual);
@ -58,7 +83,7 @@ export default function(ownProps: any) {
}, [bucketAddress, recipientAddress]);
if (props.error) {
return `Error: ${errorMessage(props.error)}`;
return `Error: ${buckerErrorMessage(props.error)}`;
}
if (props.loading) {
@ -75,6 +100,7 @@ export default function(ownProps: any) {
Bucket Address: {props.bucketAddress}<br />
Recipient: {props.recipient}<br />
Amount: {props.amount}<br />
Expiration Time: {new Date(props.expirationTime * 1000).toLocaleDateString("default", {hour: "numeric", minute: "numeric"})}<br />
Code Hash: {props.codeHash}<br />
Token Address: {props.tokenAddress}<br />
Token Symbol: {props.tokenSymbol}<br />
@ -84,5 +110,14 @@ export default function(ownProps: any) {
Receiver: {props.receiver} <br />
<br /><br /><br />
<button
disabled={props.redeeming}
onClick={() => dispatch(redeem(bucketAddress, recipientAddress, REDEEM_CODE))}>
{props.redeeming ? "Redeeming..." : "Redeem"}
</button>
<br />
{props.redeemError && `Error: ${redeemErrorMessage(props.redeemError)}`}
{props.redeemTxHash && `Done! Tx Hash: ${props.redeemTxHash}`}
</>;
}

View File

@ -1,6 +1,6 @@
import {
BucketActions,
BucketError,
BucketErrors,
BUCKET_GIFT_LOADING,
BUCKET_GIFT_NOT_FOUND,
BUCKET_GIFT_LOADED,
@ -11,9 +11,11 @@ import {
export interface BucketState {
loading: boolean
address: string | undefined
expirationTime: number | undefined
tokenAddress: string | undefined
tokenSymbol: string | undefined
tokenDecimals: number | undefined
error: BucketState | undefined
error: BucketErrors | undefined
recipient: string | undefined
amount: string | undefined
codeHash: string | undefined
@ -22,7 +24,9 @@ export interface BucketState {
const initialState: BucketState = {
loading: false,
address: undefined,
expirationTime: undefined,
tokenAddress: undefined,
tokenSymbol: undefined,
tokenDecimals: undefined,
error: undefined,
recipient: undefined,
@ -37,6 +41,7 @@ export const bucketReducer = (state: BucketState = initialState, action: BucketA
...initialState,
loading: true,
address: action.address,
recipient: action.recipient,
}
}
@ -52,6 +57,7 @@ export const bucketReducer = (state: BucketState = initialState, action: BucketA
return {
...state,
loading: false,
expirationTime: action.expirationTime,
recipient: action.recipient,
amount: action.amount,
codeHash: action.codeHash,

View File

@ -8,10 +8,15 @@ import {
BucketState,
bucketReducer,
} from './bucket';
import {
RedeemState,
redeemReducer,
} from './redeem';
export interface RootState {
web3: Web3State,
bucket: BucketState,
redeem: RedeemState,
}
export default function(history) {
@ -19,5 +24,6 @@ export default function(history) {
web3: web3Reducer,
router: connectRouter(history),
bucket: bucketReducer,
redeem: redeemReducer,
});
}

60
app/js/reducers/redeem.ts Normal file
View File

@ -0,0 +1,60 @@
import {
RedeemActions,
RedeemErrors,
REDEEM_LOADING,
REDEEM_ERROR,
REDEEM_DONE,
} from "../actions/redeem";
import {
BucketGiftLoadingAction,
BUCKET_GIFT_LOADING
} from "../actions/bucket";
export interface RedeemState {
loading: boolean
error: RedeemErrors | undefined
txHash: string | undefined
receiver: string | undefined
}
const initialState: RedeemState = {
loading: false,
error: undefined,
txHash: undefined,
receiver: undefined,
}
export const redeemReducer = (state: RedeemState = initialState, action: RedeemActions | BucketGiftLoadingAction): RedeemState => {
switch (action.type) {
case BUCKET_GIFT_LOADING: {
return initialState;
}
case REDEEM_LOADING: {
return {
...initialState,
loading: true,
}
}
case REDEEM_ERROR: {
return {
...initialState,
loading: false,
error: action.error,
}
}
case REDEEM_DONE: {
return {
...initialState,
loading: false,
txHash: action.txHash,
}
}
default:
return state;
}
}

View File

@ -4,6 +4,7 @@ import {
WEB3_ERROR,
WEB3_NETWORK_ID_LOADED,
WEB3_ACCOUNT_LOADED,
Web3Type,
} from '../actions/web3';
export interface Web3State {
@ -11,6 +12,7 @@ export interface Web3State {
networkID: number | undefined
error: string | undefined
account: string | undefined
type: Web3Type
}
const initialState: Web3State = {
@ -18,6 +20,7 @@ const initialState: Web3State = {
networkID: undefined,
error: undefined,
account: undefined,
type: Web3Type.None,
};
export const web3Reducer = (state: Web3State = initialState, action: Web3Actions): Web3State => {
@ -26,6 +29,7 @@ export const web3Reducer = (state: Web3State = initialState, action: Web3Actions
return {
...state,
initialized: true,
type: action.web3Type,
}
}

View File

@ -11,6 +11,7 @@
"dependencies": {
"connected-react-router": "^6.7.0",
"esm": "^3.2.25",
"eth-sig-util": "^2.5.3",
"history": "^4.10.1",
"minimist": "^1.2.0",
"react": "^16.12.0",

View File

@ -43,29 +43,29 @@ let sendMethod;
async function signRedeem(contractAddress, signer, message) {
const result = await web3.eth.net.getId();
let chainId = parseInt(result);
//FIXME: getChainID in the contract returns 1 so we hardcode it here to 1.
//FIXME: in tests, getChainID in the contract returns 1 so we hardcode it here to 1.
chainId = 1;
let domain = [
const domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" }
];
let redeem = [
const redeem = [
{ name: "receiver", type: "address" },
{ name: "code", type: "bytes32" },
];
let domainData = {
const domainData = {
name: "KeycardGift",
version: "1",
chainId: chainId,
verifyingContract: contractAddress
};
let data = {
const data = {
types: {
EIP712Domain: domain,
Redeem: redeem,

View File

@ -4881,6 +4881,18 @@ eth-sig-util@^2.1.1:
tweetnacl "^1.0.0"
tweetnacl-util "^0.15.0"
eth-sig-util@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-2.5.3.tgz#6938308b38226e0b3085435474900b03036abcbe"
integrity sha512-KpXbCKmmBUNUTGh9MRKmNkIPietfhzBqqYqysDavLseIiMUGl95k6UcPEkALAZlj41e9E6yioYXc1PC333RKqw==
dependencies:
buffer "^5.2.1"
elliptic "^6.4.0"
ethereumjs-abi "0.6.5"
ethereumjs-util "^5.1.1"
tweetnacl "^1.0.0"
tweetnacl-util "^0.15.0"
ethashjs@~0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ethashjs/-/ethashjs-0.0.7.tgz#30bfe4196726690a0c59d3b8272e70d4d0c34bae"