Merge branch 'master' of github.com:status-im/keycard-redeem
This commit is contained in:
commit
60133de7d1
|
@ -1,10 +0,0 @@
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Keycard Redeem</title>
|
|
||||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script src="/js/app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -36,6 +36,19 @@ module.exports = {
|
||||||
ERC20BucketFactory: {
|
ERC20BucketFactory: {
|
||||||
params: [],
|
params: [],
|
||||||
},
|
},
|
||||||
|
ERC20Bucket: {
|
||||||
|
params: [],
|
||||||
|
proxyFor: "Bucket",
|
||||||
|
deploy: false,
|
||||||
|
},
|
||||||
|
NFTBucketFactory: {
|
||||||
|
params: [],
|
||||||
|
},
|
||||||
|
NFTBucket: {
|
||||||
|
params: [],
|
||||||
|
proxyFor: "Bucket",
|
||||||
|
deploy: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,8 @@ module.exports = {
|
||||||
// A new DApp can be created from that template with:
|
// A new DApp can be created from that template with:
|
||||||
// embark new --template typescript
|
// embark new --template typescript
|
||||||
// NOTE: the `--template` option is DEPRECATED in v5.
|
// NOTE: the `--template` option is DEPRECATED in v5.
|
||||||
enabled: true
|
enabled: false
|
||||||
// Setting `enabled: false` in this config will disable Embark's built-in Webpack
|
// Setting `enabled: false` in this config will disable Embark's built-in Webpack
|
||||||
// pipeline. The developer will need to use a different frontend build tool, such as
|
// pipeline. The developer will need to use a different frontend build tool, such as
|
||||||
// `create-react-app` or Angular CLI to build their dapp
|
// `create-react-app` or Angular CLI to build their dapp
|
||||||
};
|
};
|
||||||
|
|
|
@ -67,14 +67,21 @@ abstract contract Bucket {
|
||||||
function bucketType() virtual external returns (uint256);
|
function bucketType() virtual external returns (uint256);
|
||||||
|
|
||||||
function redeem(Redeem calldata _redeem, bytes calldata _sig) external {
|
function redeem(Redeem calldata _redeem, bytes calldata _sig) external {
|
||||||
validateRedeem(_redeem, maxTxDelayInBlocks, expirationTime, startTime);
|
// validate Redeem
|
||||||
|
require(_redeem.blockNumber < block.number, "transaction cannot be in the future");
|
||||||
|
require(_redeem.blockNumber >= (block.number - maxTxDelayInBlocks), "transaction too old");
|
||||||
|
require(_redeem.blockHash == blockhash(_redeem.blockNumber), "invalid block hash");
|
||||||
|
require(block.timestamp < expirationTime, "expired redeemable");
|
||||||
|
require(block.timestamp > startTime, "reedeming not yet started");
|
||||||
|
|
||||||
address recipient = recoverSigner(DOMAIN_SEPARATOR, _redeem, _sig);
|
address recipient = recoverSigner(DOMAIN_SEPARATOR, _redeem, _sig);
|
||||||
|
|
||||||
Redeemable storage redeemable = redeemables[recipient];
|
Redeemable storage redeemable = redeemables[recipient];
|
||||||
require(redeemable.recipient == recipient, "not found");
|
require(redeemable.recipient == recipient, "not found");
|
||||||
|
|
||||||
validateCode(_redeem, redeemable.code);
|
// validate code
|
||||||
|
bytes32 codeHash = keccak256(abi.encodePacked(_redeem.code));
|
||||||
|
require(codeHash == redeemable.code, "invalid code");
|
||||||
|
|
||||||
uint256 data = redeemable.data;
|
uint256 data = redeemable.data;
|
||||||
|
|
||||||
|
@ -109,15 +116,6 @@ abstract contract Bucket {
|
||||||
require(block.timestamp >= _expirationTime, "not expired yet");
|
require(block.timestamp >= _expirationTime, "not expired yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateRedeem(Redeem memory _redeem, uint256 _maxTxDelayInBlocks, uint256 _expirationTime, uint256 _startTime) internal view {
|
|
||||||
require(_redeem.blockNumber < block.number, "transaction cannot be in the future");
|
|
||||||
require(_redeem.blockNumber >= (block.number - _maxTxDelayInBlocks), "transaction too old");
|
|
||||||
require(_redeem.blockHash == blockhash(_redeem.blockNumber), "invalid block hash");
|
|
||||||
|
|
||||||
require(block.timestamp < _expirationTime, "expired redeemable");
|
|
||||||
require(block.timestamp > _startTime, "reedeming not yet started");
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashRedeem(Redeem memory _redeem) internal pure returns (bytes32) {
|
function hashRedeem(Redeem memory _redeem) internal pure returns (bytes32) {
|
||||||
return keccak256(abi.encode(
|
return keccak256(abi.encode(
|
||||||
REDEEM_TYPEHASH,
|
REDEEM_TYPEHASH,
|
||||||
|
@ -155,9 +153,4 @@ abstract contract Bucket {
|
||||||
|
|
||||||
return ecrecover(digest, v, r, s);
|
return ecrecover(digest, v, r, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(Redeem memory _redeem, bytes32 _code) internal pure {
|
|
||||||
bytes32 codeHash = keccak256(abi.encodePacked(_redeem.code));
|
|
||||||
require(codeHash == _code, "invalid code");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
{
|
{
|
||||||
"contracts": ["contracts/**"],
|
"contracts": ["contracts/**"],
|
||||||
"app": {
|
|
||||||
"css/app.css": ["app/css/**"],
|
|
||||||
"js/app.js": ["app/js/index.tsx"],
|
|
||||||
"images/": ["app/images/**"],
|
|
||||||
"index.html": "app/index.html"
|
|
||||||
},
|
|
||||||
"buildDir": "dist/",
|
"buildDir": "dist/",
|
||||||
"config": "config/",
|
"config": "config/",
|
||||||
"versions": {
|
"versions": {
|
||||||
|
@ -26,5 +20,5 @@
|
||||||
"optimize-runs": 200
|
"optimize-runs": 200
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"generationDir": "embarkArtifacts"
|
"generationDir": "src/embarkArtifacts"
|
||||||
}
|
}
|
||||||
|
|
39
package.json
39
package.json
|
@ -3,28 +3,42 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "embark test"
|
"test": "embark test",
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/history": "^4.7.5",
|
||||||
|
"@types/react": "^16.9.0",
|
||||||
|
"@types/react-dom": "^16.9.0",
|
||||||
|
"@types/react-redux": "^7.1.7",
|
||||||
|
"@types/react-router-dom": "^5.1.5",
|
||||||
|
"bn.js": "^5.1.1",
|
||||||
"connected-react-router": "^6.7.0",
|
"connected-react-router": "^6.7.0",
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"eth-sig-util": "^2.5.3",
|
"eth-sig-util": "^2.5.3",
|
||||||
"history": "^4.10.1",
|
"history": "^4.10.1",
|
||||||
"minimist": "^1.2.3",
|
"minimist": "^1.2.3",
|
||||||
"react": "^16.12.0",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.13.1",
|
||||||
"react-redux": "^7.2.0",
|
"react-redux": "^7.2.0",
|
||||||
"react-router": "^5.1.2",
|
"react-router": "^5.1.2",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
|
"react-scripts": "3.4.1",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"web3": "^1.2.6"
|
"typescript": "^3.8.3",
|
||||||
|
"web3": "^1.2.6",
|
||||||
|
"web3-eth": "^1.2.6"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.8.3",
|
|
||||||
"embark": "^5.3.0-nightly.7",
|
"embark": "^5.3.0-nightly.7",
|
||||||
"embark-geth": "^5.3.0-nightly.7",
|
"embark-geth": "^5.3.0-nightly.7",
|
||||||
"embark-graph": "^5.3.0-nightly.7",
|
"embark-graph": "^5.3.0-nightly.7",
|
||||||
|
@ -38,7 +52,18 @@
|
||||||
"embarkjs-ipfs": "^5.3.0-nightly.4",
|
"embarkjs-ipfs": "^5.3.0-nightly.4",
|
||||||
"embarkjs-swarm": "^5.3.0-nightly.4",
|
"embarkjs-swarm": "^5.3.0-nightly.4",
|
||||||
"embarkjs-web3": "^5.3.0-nightly.4",
|
"embarkjs-web3": "^5.3.0-nightly.4",
|
||||||
"embarkjs-whisper": "^5.3.0-nightly.4",
|
"embarkjs-whisper": "^5.3.0-nightly.4"
|
||||||
"typescript": "^3.8.3"
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<title>Keycard Redeem</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/css/app.css" type="text/css" media="screen" charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -195,4 +195,4 @@ async function run() {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { RootState } from '../reducers';
|
import { RootState } from '../reducers';
|
||||||
import ERC20Bucket from '../../../embarkArtifacts/contracts/ERC20Bucket';
|
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 { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
|
|
||||||
export const ERROR_REDEEMABLE_NOT_FOUND = "ERROR_REDEEMABLE_NOT_FOUND";
|
export const ERROR_REDEEMABLE_NOT_FOUND = "ERROR_REDEEMABLE_NOT_FOUND";
|
||||||
|
@ -129,9 +128,10 @@ export const loadRedeemable = (bucketAddress: string, recipientAddress: string)
|
||||||
return async (dispatch: Dispatch, getState: () => RootState) => {
|
return async (dispatch: Dispatch, getState: () => RootState) => {
|
||||||
dispatch(loadingRedeemable(bucketAddress, recipientAddress));
|
dispatch(loadingRedeemable(bucketAddress, recipientAddress));
|
||||||
const bucket = newBucketContract(bucketAddress);
|
const bucket = newBucketContract(bucketAddress);
|
||||||
bucket.methods.expirationTime().call().then(expirationTime => {
|
bucket.methods.expirationTime().call().then((expirationTime: number) => {
|
||||||
bucket.methods.redeemables(recipientAddress).call().then((result: any) => {
|
bucket.methods.redeemables(recipientAddress).call().then((result: any) => {
|
||||||
const { recipient, amount, code } = result;
|
const { recipient, data, code } = result;
|
||||||
|
const amount = data;
|
||||||
if (amount === "0") {
|
if (amount === "0") {
|
||||||
dispatch(redeemableNotFound())
|
dispatch(redeemableNotFound())
|
||||||
return;
|
return;
|
||||||
|
@ -139,20 +139,21 @@ export const loadRedeemable = (bucketAddress: string, recipientAddress: string)
|
||||||
|
|
||||||
dispatch(redeemableLoaded(expirationTime, recipient, amount, code));
|
dispatch(redeemableLoaded(expirationTime, recipient, amount, code));
|
||||||
dispatch<any>(loadToken(bucket))
|
dispatch<any>(loadToken(bucket))
|
||||||
}).catch(err => {
|
}).catch((err: string) => {
|
||||||
dispatch(errorLoadingRedeemable(err))
|
dispatch(errorLoadingRedeemable(err))
|
||||||
console.error("err: ", err)
|
console.error("err: ", err)
|
||||||
})
|
})
|
||||||
}).catch(err => {
|
}).catch((err: string) => {
|
||||||
dispatch(errorLoadingRedeemable(`error loading expirationTime: ${err}`))
|
dispatch(errorLoadingRedeemable(`error loading expirationTime: ${err}`))
|
||||||
console.error("err: ", err)
|
console.error("err: ", err)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadToken = (bucket: Contract) => {
|
//FIXME: set the proper Contract type
|
||||||
|
export const loadToken = (bucket: any) => {
|
||||||
return (dispatch: Dispatch, getState: () => RootState) => {
|
return (dispatch: Dispatch, getState: () => RootState) => {
|
||||||
bucket.methods.tokenContract().call().then(async (address: string) => {
|
bucket.methods.tokenAddress().call().then(async (address: string) => {
|
||||||
const erc20Abi = IERC20Detailed.options.jsonInterface;
|
const erc20Abi = IERC20Detailed.options.jsonInterface;
|
||||||
const erc20 = new config.web3!.eth.Contract(erc20Abi, address);
|
const erc20 = new config.web3!.eth.Contract(erc20Abi, address);
|
||||||
dispatch(loadingToken(address));
|
dispatch(loadingToken(address));
|
|
@ -1,12 +1,12 @@
|
||||||
import { RootState } from '../reducers';
|
import { RootState } from '../reducers';
|
||||||
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';
|
||||||
import { newBucketContract } from "./bucket";
|
import { newBucketContract } from "./bucket";
|
||||||
import { sha3 } from "web3-utils";
|
import { sha3 } from "web3-utils";
|
||||||
import { recoverTypedSignature } from 'eth-sig-util';
|
import { recoverTypedSignature } from 'eth-sig-util';
|
||||||
import { Web3Type } from "../actions/web3";
|
import { Web3Type } from "../actions/web3";
|
||||||
|
import { KECCAK_EMPTY_STRING } from '../utils';
|
||||||
|
|
||||||
const sleep = (ms: number) => {
|
const sleep = (ms: number) => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
@ -91,31 +91,37 @@ const redeemDone = (txHash: string) => ({
|
||||||
txHash,
|
txHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const redeem = (bucketAddress: string, recipientAddress: string, code: string) => {
|
export const redeem = (bucketAddress: string, recipientAddress: string, cleanCode: string) => {
|
||||||
return async (dispatch: Dispatch, getState: () => RootState) => {
|
return async (dispatch: Dispatch, getState: () => RootState) => {
|
||||||
|
let finalCode;
|
||||||
|
if (cleanCode === "") {
|
||||||
|
finalCode = KECCAK_EMPTY_STRING;
|
||||||
|
} else {
|
||||||
|
finalCode = sha3(cleanCode);
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(redeeming());
|
dispatch(redeeming());
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const web3Type = state.web3.type;
|
const web3Type = state.web3.type;
|
||||||
const bucketAddress = state.bucket.address;
|
|
||||||
const bucket = newBucketContract(bucketAddress);
|
const bucket = newBucketContract(bucketAddress);
|
||||||
const codeHash = sha3(code);
|
|
||||||
const account = state.web3.account;
|
const account = state.web3.account;
|
||||||
const block = await config.web3!.eth.getBlock("latest");
|
const block = await config.web3!.eth.getBlock("latest");
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
receiver: state.web3.account,
|
receiver: state.web3.account!,
|
||||||
code: codeHash,
|
code: finalCode!,
|
||||||
blockNumber: block.number,
|
blockNumber: block.number,
|
||||||
blockHash: block.hash,
|
blockHash: block.hash,
|
||||||
};
|
};
|
||||||
|
|
||||||
//FIXME: is signer needed?
|
//FIXME: is signer needed?
|
||||||
signRedeem(web3Type, bucketAddress, state.web3.account, message).then(async ({ sig, address }: SignRedeemResponse) => {
|
signRedeem(web3Type, bucketAddress, state.web3.account!, message).then(async ({ sig, address }: SignRedeemResponse) => {
|
||||||
const recipient = state.bucket.recipient;
|
const recipient = state.bucket.recipient!;
|
||||||
//FIXME: remove! hack to wait for the request screen to slide down
|
//FIXME: remove! hack to wait for the request screen to slide down
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
|
|
||||||
if (address.toLowerCase() != recipient.toLowerCase()) {
|
if (address.toLowerCase() !== recipient.toLowerCase()) {
|
||||||
//FIXME: handle error
|
//FIXME: handle error
|
||||||
dispatch(wrongSigner(recipient, address));
|
dispatch(wrongSigner(recipient, address));
|
||||||
return;
|
return;
|
||||||
|
@ -123,13 +129,16 @@ export const redeem = (bucketAddress: string, recipientAddress: string, code: st
|
||||||
|
|
||||||
const redeem = bucket.methods.redeem(message, sig);
|
const redeem = bucket.methods.redeem(message, sig);
|
||||||
const gas = await redeem.estimateGas();
|
const gas = await redeem.estimateGas();
|
||||||
redeem.send({ from: account, gas }).then(resp => {
|
redeem.send({
|
||||||
|
from: account,
|
||||||
|
gas
|
||||||
|
}).then((resp: any) => {
|
||||||
dispatch(redeemDone(resp.transactionHash));
|
dispatch(redeemDone(resp.transactionHash));
|
||||||
}).catch(err => {
|
}).catch((err: string) => {
|
||||||
console.error("redeem error: ", err);
|
console.error("redeem error: ", err);
|
||||||
dispatch(redeemError(err))
|
dispatch(redeemError(err))
|
||||||
});
|
});
|
||||||
}).catch(err => {
|
}).catch((err: string) => {
|
||||||
console.error("sign redeem error: ", err);
|
console.error("sign redeem error: ", err);
|
||||||
dispatch(redeemError(err))
|
dispatch(redeemError(err))
|
||||||
});
|
});
|
||||||
|
@ -183,7 +192,7 @@ const signWithWeb3 = (signer: string, data: any): Promise<SignRedeemResponse> =>
|
||||||
method: "eth_signTypedData_v3",
|
method: "eth_signTypedData_v3",
|
||||||
params: [signer, JSON.stringify(data)],
|
params: [signer, JSON.stringify(data)],
|
||||||
from: signer,
|
from: signer,
|
||||||
}, (err, resp) => {
|
}, (err: string, resp: any) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
|
@ -201,14 +210,14 @@ const signWithWeb3 = (signer: string, data: any): Promise<SignRedeemResponse> =>
|
||||||
|
|
||||||
const signWithKeycard = (signer: string, data: any): Promise<SignRedeemResponse> => {
|
const signWithKeycard = (signer: string, data: any): Promise<SignRedeemResponse> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
(window as any).ethereum.send("keycard_signTypedData", [signer, JSON.stringify(data)]).then(resp => {
|
(window as any).ethereum.send("keycard_signTypedData", [signer, JSON.stringify(data)]).then((resp: any) => {
|
||||||
const sig = resp.result;
|
const sig = resp.result;
|
||||||
const address = recoverTypedSignature({
|
const address = recoverTypedSignature({
|
||||||
data,
|
data,
|
||||||
sig
|
sig
|
||||||
});
|
});
|
||||||
resolve({ sig, address });
|
resolve({ sig, address });
|
||||||
}).catch(err => {
|
}).catch((err: string) => {
|
||||||
reject(err);
|
reject(err);
|
||||||
})
|
})
|
||||||
});
|
});
|
|
@ -1,9 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ERC20BucketFactory from '../../../embarkArtifacts/contracts/ERC20BucketFactory';
|
import ERC20BucketFactory from '../embarkArtifacts/contracts/ERC20BucketFactory';
|
||||||
|
import { RootState } from '../reducers';
|
||||||
import {
|
import {
|
||||||
shallowEqual,
|
shallowEqual,
|
||||||
useSelector,
|
useSelector,
|
||||||
useDispatch,
|
|
||||||
} from 'react-redux';
|
} from 'react-redux';
|
||||||
import { Web3Type } from "../actions/web3";
|
import { Web3Type } from "../actions/web3";
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ const web3Type = (t: Web3Type) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function(ownProps: any) {
|
export default function(ownProps: any) {
|
||||||
const props = useSelector(state => {
|
const props = useSelector((state: RootState) => {
|
||||||
return {
|
return {
|
||||||
initialized: state.web3.networkID,
|
initialized: state.web3.networkID,
|
||||||
networkID: state.web3.networkID,
|
networkID: state.web3.networkID,
|
||||||
|
@ -31,11 +31,11 @@ export default function(ownProps: any) {
|
||||||
}, shallowEqual);
|
}, shallowEqual);
|
||||||
|
|
||||||
if (props.error) {
|
if (props.error) {
|
||||||
return `Error: ${props.error}`;
|
return <>Error: {props.error}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.initialized) {
|
if (!props.initialized) {
|
||||||
return "initializing...";
|
return <>initializing...</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
|
@ -1,16 +1,23 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
class ErrorBoundary extends React.Component {
|
interface Props {
|
||||||
constructor(props) {
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component<Props, State> {
|
||||||
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { hasError: false };
|
this.state = { hasError: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromError(error) {
|
static getDerivedStateFromError(error: any) {
|
||||||
return { hasError: true };
|
return { hasError: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error, errorInfo) {
|
componentDidCatch(error: any, errorInfo: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,5 @@ export default function() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<p>
|
|
||||||
</p>
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
|
@ -13,7 +13,10 @@ import {
|
||||||
ERROR_LOADING_REDEEMABLE,
|
ERROR_LOADING_REDEEMABLE,
|
||||||
ERROR_REDEEMABLE_NOT_FOUND,
|
ERROR_REDEEMABLE_NOT_FOUND,
|
||||||
} from '../actions/bucket';
|
} from '../actions/bucket';
|
||||||
import { toBaseUnit } from "../utils";
|
import {
|
||||||
|
toBaseUnit,
|
||||||
|
KECCAK_EMPTY_STRING2,
|
||||||
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
redeem,
|
redeem,
|
||||||
RedeemErrors,
|
RedeemErrors,
|
||||||
|
@ -21,8 +24,6 @@ import {
|
||||||
ERROR_WRONG_SIGNER,
|
ERROR_WRONG_SIGNER,
|
||||||
} from '../actions/redeem';
|
} from '../actions/redeem';
|
||||||
|
|
||||||
const REDEEM_CODE = "hello world";
|
|
||||||
|
|
||||||
const buckerErrorMessage = (error: BucketErrors): string => {
|
const buckerErrorMessage = (error: BucketErrors): string => {
|
||||||
switch (error.type) {
|
switch (error.type) {
|
||||||
case ERROR_LOADING_REDEEMABLE:
|
case ERROR_LOADING_REDEEMABLE:
|
||||||
|
@ -49,13 +50,23 @@ const redeemErrorMessage = (error: RedeemErrors): string => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface URLParams {
|
||||||
|
bucketAddress: string
|
||||||
|
recipientAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function(ownProps: any) {
|
export default function(ownProps: any) {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const match = useRouteMatch({
|
|
||||||
|
const match = useRouteMatch<URLParams>({
|
||||||
path: redeemPath,
|
path: redeemPath,
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const bucketAddress = match.params.bucketAddress;
|
const bucketAddress = match.params.bucketAddress;
|
||||||
const recipientAddress = match.params.recipientAddress;
|
const recipientAddress = match.params.recipientAddress;
|
||||||
|
|
||||||
|
@ -78,30 +89,32 @@ export default function(ownProps: any) {
|
||||||
}
|
}
|
||||||
}, shallowEqual);
|
}, shallowEqual);
|
||||||
|
|
||||||
|
const emptyCode = props.codeHash === KECCAK_EMPTY_STRING2;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(loadRedeemable(bucketAddress, recipientAddress));
|
dispatch(loadRedeemable(bucketAddress, recipientAddress));
|
||||||
}, [bucketAddress, recipientAddress]);
|
}, [dispatch, bucketAddress, recipientAddress]);
|
||||||
|
|
||||||
if (props.error) {
|
if (props.error) {
|
||||||
return `Error: ${buckerErrorMessage(props.error)}`;
|
return <>Error: {buckerErrorMessage(props.error)}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.loading) {
|
if (props.loading) {
|
||||||
return "loading bucket...";
|
return <>loading bucket...</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.tokenSymbol === undefined || props.tokenDecimals === undefined) {
|
if (props.tokenSymbol === undefined || props.tokenDecimals === undefined) {
|
||||||
return "loading token info...";
|
return <>loading token info...</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [displayAmount, roundedDisplayAmount] = toBaseUnit(props.amount, props.tokenDecimals, 2);
|
const [displayAmount, roundedDisplayAmount] = toBaseUnit(props.amount!, props.tokenDecimals, 2);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
Bucket Address: {props.bucketAddress}<br />
|
Bucket Address: {props.bucketAddress}<br />
|
||||||
Recipient: {props.recipient}<br />
|
Recipient: {props.recipient}<br />
|
||||||
Amount: {props.amount}<br />
|
Amount: {props.amount}<br />
|
||||||
Expiration Time: {new Date(props.expirationTime * 1000).toLocaleDateString("default", {hour: "numeric", minute: "numeric"})}<br />
|
Expiration Time: {new Date(props.expirationTime! * 1000).toLocaleDateString("default", {hour: "numeric", minute: "numeric"})}<br />
|
||||||
Code Hash: {props.codeHash}<br />
|
Code Hash: {props.codeHash} {emptyCode ? "(empty string)" : ""}<br />
|
||||||
Token Address: {props.tokenAddress}<br />
|
Token Address: {props.tokenAddress}<br />
|
||||||
Token Symbol: {props.tokenSymbol}<br />
|
Token Symbol: {props.tokenSymbol}<br />
|
||||||
Token Decimals: {props.tokenDecimals}<br />
|
Token Decimals: {props.tokenDecimals}<br />
|
||||||
|
@ -112,7 +125,7 @@ export default function(ownProps: any) {
|
||||||
<br /><br /><br />
|
<br /><br /><br />
|
||||||
<button
|
<button
|
||||||
disabled={props.redeeming}
|
disabled={props.redeeming}
|
||||||
onClick={() => dispatch(redeem(bucketAddress, recipientAddress, REDEEM_CODE))}>
|
onClick={() => dispatch(redeem(bucketAddress, recipientAddress, ""))}>
|
||||||
{props.redeeming ? "Redeeming..." : "Redeem"}
|
{props.redeeming ? "Redeeming..." : "Redeem"}
|
||||||
</button>
|
</button>
|
||||||
<br />
|
<br />
|
|
@ -1,4 +1,3 @@
|
||||||
import EmbarkJS from 'Embark/EmbarkJS';
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import thunkMiddleware from 'redux-thunk';
|
import thunkMiddleware from 'redux-thunk';
|
||||||
|
@ -41,23 +40,21 @@ const store = createStore(
|
||||||
applyMiddleware(...middlewares),
|
applyMiddleware(...middlewares),
|
||||||
);
|
);
|
||||||
|
|
||||||
EmbarkJS.onReady(err => {
|
store.dispatch<any>(initializeWeb3());
|
||||||
store.dispatch<any>(initializeWeb3());
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<App>
|
<App>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/"><Home /></Route>
|
<Route exact path="/"><Home /></Route>
|
||||||
<Route exact path={redeemPath}><Redeem /></Route>
|
<Route exact path={redeemPath}><Redeem /></Route>
|
||||||
<Route render={() => "page not found"} />
|
<Route render={() => "page not found"} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</App>
|
</App>
|
||||||
</Provider>
|
</Provider>
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
);
|
);
|
||||||
});
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import { connectRouter } from 'connected-react-router';
|
import { connectRouter } from 'connected-react-router';
|
||||||
|
import { History } from 'history';
|
||||||
import {
|
import {
|
||||||
Web3State,
|
Web3State,
|
||||||
web3Reducer,
|
web3Reducer,
|
||||||
|
@ -19,7 +20,7 @@ export interface RootState {
|
||||||
redeem: RedeemState,
|
redeem: RedeemState,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function(history) {
|
export default function(history: History) {
|
||||||
return combineReducers({
|
return combineReducers({
|
||||||
web3: web3Reducer,
|
web3: web3Reducer,
|
||||||
router: connectRouter(history),
|
router: connectRouter(history),
|
|
@ -1,6 +1,9 @@
|
||||||
import Web3Utils from "web3-utils";
|
import { sha3 } from "web3-utils";
|
||||||
|
import BN from "bn.js";
|
||||||
|
|
||||||
const BN = Web3Utils.BN;
|
// keccak256("")
|
||||||
|
export const KECCAK_EMPTY_STRING = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470";
|
||||||
|
export const KECCAK_EMPTY_STRING2 = sha3(KECCAK_EMPTY_STRING);
|
||||||
|
|
||||||
export const toBaseUnit = (fullAmount: string, decimalsSize: number, roundDecimals: number) => {
|
export const toBaseUnit = (fullAmount: string, decimalsSize: number, roundDecimals: number) => {
|
||||||
const amount = new BN(fullAmount);
|
const amount = new BN(fullAmount);
|
|
@ -1,188 +0,0 @@
|
||||||
// some packages, plugins, and presets referenced/required in this webpack
|
|
||||||
// config are deps of embark and will be transitive dapp deps unless specified
|
|
||||||
// in the dapp's own package.json
|
|
||||||
|
|
||||||
// embark modifies process.env.NODE_PATH so that when running dapp scripts in
|
|
||||||
// embark's child processes, embark's own node_modules directory will be
|
|
||||||
// searched by node's require(); however, webpack and babel do not directly
|
|
||||||
// support NODE_PATH, so modules such as babel plugins and presets must be
|
|
||||||
// resolved with require.resolve(); that is only necessary if a plugin/preset
|
|
||||||
// is in embark's node_modules vs. the dapp's node_modules
|
|
||||||
|
|
||||||
const cloneDeep = require('lodash.clonedeep');
|
|
||||||
// const CompressionPlugin = require('compression-webpack-plugin');
|
|
||||||
const glob = require('glob');
|
|
||||||
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const dappPath = process.env.DAPP_PATH;
|
|
||||||
const embarkPath = process.env.EMBARK_PATH;
|
|
||||||
|
|
||||||
const embarkAliases = require(path.join(dappPath, '.embark/embark-aliases.json'));
|
|
||||||
const embarkAssets = require(path.join(dappPath, '.embark/embark-assets.json'));
|
|
||||||
const embarkNodeModules = path.join(embarkPath, 'node_modules');
|
|
||||||
const embarkJson = require(path.join(dappPath, 'embark.json'));
|
|
||||||
|
|
||||||
const buildDir = path.join(dappPath, embarkJson.buildDir);
|
|
||||||
|
|
||||||
// it's important to `embark reset` if a pkg version is specified in
|
|
||||||
// embark.json and changed/removed later, otherwise pkg resolution may behave
|
|
||||||
// unexpectedly
|
|
||||||
let versions;
|
|
||||||
try {
|
|
||||||
versions = glob.sync(path.join(dappPath, '.embark/versions/*/*'));
|
|
||||||
} catch (e) {
|
|
||||||
versions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = Object.keys(embarkAssets)
|
|
||||||
.filter(key => key.match(/\.js$/))
|
|
||||||
.reduce((obj, key) => {
|
|
||||||
// webpack entry paths should start with './' if they're relative to the
|
|
||||||
// webpack context; embark.json "app" keys correspond to lists of .js
|
|
||||||
// source paths relative to the top-level dapp dir and may be missing the
|
|
||||||
// leading './'
|
|
||||||
obj[key] = embarkAssets[key]
|
|
||||||
.map(file => {
|
|
||||||
let file_path = file.path;
|
|
||||||
if (!file.path.match(/^\.\//)) {
|
|
||||||
file_path = './' + file_path;
|
|
||||||
}
|
|
||||||
return file_path;
|
|
||||||
});
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
function resolve(pkgName) {
|
|
||||||
if (Array.isArray(pkgName)) {
|
|
||||||
const _pkgName = pkgName[0];
|
|
||||||
pkgName[0] = require.resolve(_pkgName);
|
|
||||||
return pkgName;
|
|
||||||
}
|
|
||||||
return require.resolve(pkgName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// base config
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const base = {
|
|
||||||
context: dappPath,
|
|
||||||
entry: entry,
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [{loader: 'style-loader'}, {loader: 'css-loader'}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.scss$/,
|
|
||||||
use: [{loader: 'style-loader'}, {loader: 'css-loader'}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
|
|
||||||
loader: 'url-loader?limit=100000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(js|jsx|tsx|ts)$/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
exclude: /(node_modules|bower_components|\.embark[\\/]versions)/,
|
|
||||||
options: {
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
'babel-plugin-module-resolver', {
|
|
||||||
'alias': embarkAliases
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'@babel/plugin-transform-runtime', {
|
|
||||||
corejs: 2,
|
|
||||||
useESModules: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
].map(resolve),
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
'@babel/preset-env', {
|
|
||||||
modules: false,
|
|
||||||
targets: {
|
|
||||||
browsers: ['last 1 version', 'not dead', '> 0.2%']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'@babel/preset-react',
|
|
||||||
'@babel/preset-typescript'
|
|
||||||
].map(resolve)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
filename: (chunkData) => chunkData.chunk.name,
|
|
||||||
// globalObject workaround for node-compatible UMD builds with webpack 4
|
|
||||||
// see: https://github.com/webpack/webpack/issues/6522#issuecomment-371120689
|
|
||||||
globalObject: 'typeof self !== \'undefined\' ? self : this',
|
|
||||||
libraryTarget: 'umd',
|
|
||||||
path: buildDir
|
|
||||||
},
|
|
||||||
plugins: [new HardSourceWebpackPlugin()],
|
|
||||||
// profiling and generating verbose stats increases build time; if stats
|
|
||||||
// are generated embark will write the output to:
|
|
||||||
// path.join(dappPath, '.embark/stats.[json,report]')
|
|
||||||
// to visualize the stats info in a browser run:
|
|
||||||
// npx webpack-bundle-analyzer .embark/stats.json <buildDir>
|
|
||||||
profile: true, stats: 'verbose',
|
|
||||||
resolve: {
|
|
||||||
alias: embarkAliases,
|
|
||||||
modules: [
|
|
||||||
...versions,
|
|
||||||
'node_modules',
|
|
||||||
embarkNodeModules
|
|
||||||
]
|
|
||||||
},
|
|
||||||
resolveLoader: {
|
|
||||||
modules: [
|
|
||||||
'node_modules',
|
|
||||||
embarkNodeModules
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// typescript mods
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
base.resolve.extensions = [
|
|
||||||
// webpack defaults
|
|
||||||
// see: https://webpack.js.org/configuration/resolve/#resolve-extensions
|
|
||||||
'.wasm', '.mjs', '.js', '.json',
|
|
||||||
// typescript extensions
|
|
||||||
'.ts', '.tsx'
|
|
||||||
];
|
|
||||||
|
|
||||||
// development config
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const development = cloneDeep(base);
|
|
||||||
// full source maps increase build time but are useful during dapp development
|
|
||||||
development.devtool = 'source-map';
|
|
||||||
development.mode = 'development';
|
|
||||||
// alternatively:
|
|
||||||
// development.mode = 'none';
|
|
||||||
development.name = 'development';
|
|
||||||
const devBabelLoader = development.module.rules[3];
|
|
||||||
devBabelLoader.options.compact = false;
|
|
||||||
|
|
||||||
// production config
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const production = cloneDeep(base);
|
|
||||||
production.mode = 'production';
|
|
||||||
production.name = 'production';
|
|
||||||
// compression of webpack's JS output not enabled by default
|
|
||||||
// production.plugins.push(new CompressionPlugin());
|
|
||||||
|
|
||||||
// export a list of named configs
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
module.exports = [
|
|
||||||
development,
|
|
||||||
production
|
|
||||||
];
|
|
Loading…
Reference in New Issue