new contracts for L2 (#25)
* use block relay instead of block * constructor => init * copied correct IERC20 * single instance contract * test requestPayment * test topup/withdraw * add new account check * add account/nutberry handling * implement blockrelay * make compatible with Ethersjs 5 * added info functionality * fix typo * add wallet creation/topup/withdraw * implement payment method * extract utility function * add OVM testing support
This commit is contained in:
parent
2df1e4ed2e
commit
711fda63bb
|
@ -0,0 +1,6 @@
|
|||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
interface IBlockRelay {
|
||||
function getNumber() external view returns (uint256);
|
||||
function getHash(uint256) external view returns (bytes32);
|
||||
}
|
|
@ -73,4 +73,4 @@ interface IERC20 {
|
|||
* a call to {approve}. `value` is the new allowance.
|
||||
*/
|
||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ pragma solidity >=0.5.0 <0.7.0;
|
|||
|
||||
import "./IERC20.sol";
|
||||
|
||||
contract ERC20Detailed is IERC20 {
|
||||
abstract contract ERC20Detailed is IERC20 {
|
||||
string private _name;
|
||||
string private _symbol;
|
||||
uint8 private _decimals;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
interface KeycardRegistry {
|
||||
function register(address _owner, address _keycard) external;
|
||||
function unregister(address _owner, address _keycard) external;
|
||||
function setOwner(address _oldOwner, address _newOwner) external;
|
||||
function setKeycard(address _oldKeycard, address _newKeycard) external;
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
interface KeycardRegistry {
|
||||
function register(address _owner, address _keycard) external;
|
||||
function unregister(address _owner, address _keycard) external;
|
||||
function setOwner(address _oldOwner, address _newOwner) external;
|
||||
function setKeycard(address _oldKeycard, address _newKeycard) external;
|
||||
}
|
|
@ -1,173 +1,178 @@
|
|||
pragma solidity >=0.5.0 <0.7.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "./KeycardRegistry.sol";
|
||||
import "./IERC20.sol";
|
||||
|
||||
contract KeycardWallet {
|
||||
event NewPayment(uint256 blockNumber, address to, address currency, uint256 amount);
|
||||
|
||||
//TODO: replace with chainid opcode
|
||||
// uint256 constant chainId = 1;
|
||||
uint256 constant chainId = 3;
|
||||
// uint256 constant chainId = 5;
|
||||
// uint256 constant chainId = 1337;
|
||||
|
||||
// must be less than 256, because the hash of older blocks cannot be retrieved
|
||||
uint256 constant maxTxDelayInBlocks = 10;
|
||||
|
||||
struct Payment {
|
||||
uint256 blockNumber;
|
||||
bytes32 blockHash;
|
||||
address currency;
|
||||
uint256 amount;
|
||||
address to;
|
||||
}
|
||||
|
||||
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
||||
bytes32 constant PAYMENT_TYPEHASH = keccak256("Payment(uint256 blockNumber,bytes32 blockHash,address currency,uint256 amount,address to)");
|
||||
bytes32 DOMAIN_SEPARATOR;
|
||||
|
||||
address public register;
|
||||
address public owner;
|
||||
address public keycard;
|
||||
mapping(address => uint) public tokenMaxTxAmount;
|
||||
uint256 public lastUsedBlockNum;
|
||||
uint256 public minBlockDistance;
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner, "owner required");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _owner, address _keycard, address _register, uint256 _minBlockDistance, address _token, uint256 _tokenMaxTxAmount) public {
|
||||
owner = _owner == address(0) ? msg.sender : _owner;
|
||||
keycard = _keycard;
|
||||
register = address(0);
|
||||
|
||||
minBlockDistance = _minBlockDistance;
|
||||
_setRegister(_register);
|
||||
lastUsedBlockNum = block.number;
|
||||
tokenMaxTxAmount[_token] = _tokenMaxTxAmount;
|
||||
}
|
||||
|
||||
function _setRegister(address _register) internal {
|
||||
if (register != address(0)) {
|
||||
KeycardRegistry(register).unregister(owner, keycard);
|
||||
}
|
||||
|
||||
if (_register != address(0) && msg.sender != _register) {
|
||||
KeycardRegistry(_register).register(owner, keycard);
|
||||
}
|
||||
|
||||
register = _register;
|
||||
|
||||
DOMAIN_SEPARATOR = keccak256(abi.encode(
|
||||
EIP712DOMAIN_TYPEHASH,
|
||||
keccak256("KeycardWallet"),
|
||||
keccak256("1"),
|
||||
chainId,
|
||||
register
|
||||
));
|
||||
}
|
||||
|
||||
function setRegister(address _register) public onlyOwner {
|
||||
_setRegister(_register);
|
||||
}
|
||||
|
||||
function setOwner(address _owner) public onlyOwner {
|
||||
if (register != address(0)) {
|
||||
KeycardRegistry(register).setOwner(owner, _owner);
|
||||
}
|
||||
|
||||
owner = _owner;
|
||||
}
|
||||
|
||||
function setKeycard(address _keycard) public onlyOwner {
|
||||
if (register != address(0)) {
|
||||
KeycardRegistry(register).setKeycard(keycard, _keycard);
|
||||
}
|
||||
|
||||
keycard = _keycard;
|
||||
}
|
||||
|
||||
function setMinBlockDistance(uint256 _minBlockDistance) public onlyOwner {
|
||||
minBlockDistance = _minBlockDistance;
|
||||
}
|
||||
|
||||
function setTokenMaxTXAmount(address _token, uint256 _maxTxAmount) public onlyOwner {
|
||||
tokenMaxTxAmount[_token] = _maxTxAmount;
|
||||
}
|
||||
|
||||
function hash(Payment memory _payment) internal pure returns (bytes32) {
|
||||
return keccak256(abi.encode(
|
||||
PAYMENT_TYPEHASH,
|
||||
_payment.blockNumber,
|
||||
_payment.blockHash,
|
||||
_payment.currency,
|
||||
_payment.amount,
|
||||
_payment.to
|
||||
));
|
||||
}
|
||||
|
||||
function verify(Payment memory _payment, bytes memory _sig) internal view returns (bool) {
|
||||
require(_sig.length == 65, "bad signature length");
|
||||
|
||||
bytes32 r;
|
||||
bytes32 s;
|
||||
uint8 v;
|
||||
|
||||
assembly {
|
||||
r := mload(add(_sig, 32))
|
||||
s := mload(add(_sig, 64))
|
||||
v := byte(0, mload(add(_sig, 96)))
|
||||
}
|
||||
|
||||
if (v < 27) {
|
||||
v += 27;
|
||||
}
|
||||
|
||||
require(v == 27 || v == 28, "signature version doesn't match");
|
||||
|
||||
bytes32 digest = keccak256(abi.encodePacked(
|
||||
"\x19\x01",
|
||||
DOMAIN_SEPARATOR,
|
||||
hash(_payment)
|
||||
));
|
||||
|
||||
return ecrecover(digest, v, r, s) == keycard;
|
||||
}
|
||||
|
||||
function requestPayment(Payment memory _payment, bytes memory _signature) public {
|
||||
// check that a keycard address has been set
|
||||
require(keycard != address(0), "keycard address not set");
|
||||
|
||||
// verify the signer
|
||||
require(verify(_payment, _signature), "signer is not the keycard");
|
||||
|
||||
// check that the block number used for signing is less than the block number
|
||||
require(_payment.blockNumber < block.number, "transaction cannot be in the future");
|
||||
|
||||
// check that the block number used is not too old
|
||||
require(_payment.blockNumber >= (block.number - maxTxDelayInBlocks), "transaction too old");
|
||||
|
||||
// check that the block number is not too near to the last one in which a tx has been processed
|
||||
require(_payment.blockNumber >= (lastUsedBlockNum + minBlockDistance), "cooldown period not expired yet");
|
||||
|
||||
// check that the blockHash is valid
|
||||
require(_payment.blockHash == blockhash(_payment.blockNumber), "invalid block hash");
|
||||
|
||||
// check that _payment.amount is not greater than the maxTxValue for this currency
|
||||
require(_payment.amount <= tokenMaxTxAmount[_payment.currency], "amount not allowed");
|
||||
|
||||
// check that balance is enough for this payment
|
||||
require(IERC20(_payment.currency).balanceOf(address(this)) >= _payment.amount, "balance is not enough");
|
||||
|
||||
// transfer token
|
||||
require(IERC20(_payment.currency).transfer(_payment.to, _payment.amount), "transfer failed");
|
||||
|
||||
// set new baseline block for checks
|
||||
lastUsedBlockNum = block.number;
|
||||
emit NewPayment(_payment.blockNumber, _payment.to, _payment.currency, _payment.amount);
|
||||
}
|
||||
}
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "./KeycardRegistry.sol";
|
||||
import "./IERC20.sol";
|
||||
import "./IBlockRelay.sol";
|
||||
|
||||
contract KeycardWallet {
|
||||
event NewPayment(uint256 blockNumber, address to, address currency, uint256 amount);
|
||||
|
||||
//TODO: replace with chainid opcode
|
||||
// uint256 constant chainId = 1;
|
||||
uint256 constant chainId = 3;
|
||||
// uint256 constant chainId = 5;
|
||||
// uint256 constant chainId = 1337;
|
||||
|
||||
// must be less than 256, because the hash of older blocks cannot be retrieved
|
||||
uint256 constant maxTxDelayInBlocks = 10;
|
||||
|
||||
struct Payment {
|
||||
uint256 blockNumber;
|
||||
bytes32 blockHash;
|
||||
address currency;
|
||||
uint256 amount;
|
||||
address to;
|
||||
}
|
||||
|
||||
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
||||
bytes32 constant PAYMENT_TYPEHASH = keccak256("Payment(uint256 blockNumber,bytes32 blockHash,address currency,uint256 amount,address to)");
|
||||
bytes32 DOMAIN_SEPARATOR;
|
||||
|
||||
address public register;
|
||||
address public blockRelay;
|
||||
address public owner;
|
||||
address public keycard;
|
||||
mapping(address => uint) public tokenMaxTxAmount;
|
||||
uint256 public lastUsedBlockNum;
|
||||
uint256 public minBlockDistance;
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner, "owner required");
|
||||
_;
|
||||
}
|
||||
|
||||
function init(address _owner, address _keycard, address _register, address _blockRelay, uint256 _minBlockDistance, address _token, uint256 _tokenMaxTxAmount) public {
|
||||
require(owner == address(0), "this function can only be invoked once");
|
||||
|
||||
owner = _owner == address(0) ? msg.sender : _owner;
|
||||
keycard = _keycard;
|
||||
register = address(0);
|
||||
blockRelay = _blockRelay;
|
||||
|
||||
minBlockDistance = _minBlockDistance;
|
||||
_setRegister(_register);
|
||||
lastUsedBlockNum = IBlockRelay(blockRelay).getNumber();
|
||||
tokenMaxTxAmount[_token] = _tokenMaxTxAmount;
|
||||
}
|
||||
|
||||
function _setRegister(address _register) internal {
|
||||
if (register != address(0)) {
|
||||
KeycardRegistry(register).unregister(owner, keycard);
|
||||
}
|
||||
|
||||
if (_register != address(0) && msg.sender != _register) {
|
||||
KeycardRegistry(_register).register(owner, keycard);
|
||||
}
|
||||
|
||||
register = _register;
|
||||
|
||||
DOMAIN_SEPARATOR = keccak256(abi.encode(
|
||||
EIP712DOMAIN_TYPEHASH,
|
||||
keccak256("KeycardWallet"),
|
||||
keccak256("1"),
|
||||
chainId,
|
||||
register
|
||||
));
|
||||
}
|
||||
|
||||
function setRegister(address _register) public onlyOwner {
|
||||
_setRegister(_register);
|
||||
}
|
||||
|
||||
function setOwner(address _owner) public onlyOwner {
|
||||
if (register != address(0)) {
|
||||
KeycardRegistry(register).setOwner(owner, _owner);
|
||||
}
|
||||
|
||||
owner = _owner;
|
||||
}
|
||||
|
||||
function setKeycard(address _keycard) public onlyOwner {
|
||||
if (register != address(0)) {
|
||||
KeycardRegistry(register).setKeycard(keycard, _keycard);
|
||||
}
|
||||
|
||||
keycard = _keycard;
|
||||
}
|
||||
|
||||
function setMinBlockDistance(uint256 _minBlockDistance) public onlyOwner {
|
||||
minBlockDistance = _minBlockDistance;
|
||||
}
|
||||
|
||||
function setTokenMaxTXAmount(address _token, uint256 _maxTxAmount) public onlyOwner {
|
||||
tokenMaxTxAmount[_token] = _maxTxAmount;
|
||||
}
|
||||
|
||||
function hash(Payment memory _payment) internal pure returns (bytes32) {
|
||||
return keccak256(abi.encode(
|
||||
PAYMENT_TYPEHASH,
|
||||
_payment.blockNumber,
|
||||
_payment.blockHash,
|
||||
_payment.currency,
|
||||
_payment.amount,
|
||||
_payment.to
|
||||
));
|
||||
}
|
||||
|
||||
function verify(Payment memory _payment, bytes memory _sig) internal view returns (bool) {
|
||||
require(_sig.length == 65, "bad signature length");
|
||||
|
||||
bytes32 r;
|
||||
bytes32 s;
|
||||
uint8 v;
|
||||
|
||||
assembly {
|
||||
r := mload(add(_sig, 32))
|
||||
s := mload(add(_sig, 64))
|
||||
v := byte(0, mload(add(_sig, 96)))
|
||||
}
|
||||
|
||||
if (v < 27) {
|
||||
v += 27;
|
||||
}
|
||||
|
||||
require(v == 27 || v == 28, "signature version doesn't match");
|
||||
|
||||
bytes32 digest = keccak256(abi.encodePacked(
|
||||
"\x19\x01",
|
||||
DOMAIN_SEPARATOR,
|
||||
hash(_payment)
|
||||
));
|
||||
|
||||
return ecrecover(digest, v, r, s) == keycard;
|
||||
}
|
||||
|
||||
function requestPayment(Payment memory _payment, bytes memory _signature) public {
|
||||
// check that a keycard address has been set
|
||||
require(keycard != address(0), "keycard address not set");
|
||||
|
||||
// verify the signer
|
||||
require(verify(_payment, _signature), "signer is not the keycard");
|
||||
|
||||
// check that the block number used for signing is less than the block number
|
||||
require(_payment.blockNumber < IBlockRelay(blockRelay).getNumber(), "transaction cannot be in the future");
|
||||
|
||||
// check that the block number used is not too old
|
||||
require(_payment.blockNumber >= (IBlockRelay(blockRelay).getNumber() - maxTxDelayInBlocks), "transaction too old");
|
||||
|
||||
// check that the block number is not too near to the last one in which a tx has been processed
|
||||
require(_payment.blockNumber >= (lastUsedBlockNum + minBlockDistance), "cooldown period not expired yet");
|
||||
|
||||
// check that the blockHash is valid
|
||||
require(_payment.blockHash == IBlockRelay(blockRelay).getHash(_payment.blockNumber), "invalid block hash");
|
||||
|
||||
// check that _payment.amount is not greater than the maxTxValue for this currency
|
||||
require(_payment.amount <= tokenMaxTxAmount[_payment.currency], "amount not allowed");
|
||||
|
||||
// check that balance is enough for this payment
|
||||
require(IERC20(_payment.currency).balanceOf(address(this)) >= _payment.amount, "balance is not enough");
|
||||
|
||||
// transfer token
|
||||
require(IERC20(_payment.currency).transfer(_payment.to, _payment.amount), "transfer failed");
|
||||
|
||||
// set new baseline block for checks
|
||||
lastUsedBlockNum = IBlockRelay(blockRelay).getNumber();
|
||||
emit NewPayment(_payment.blockNumber, _payment.to, _payment.currency, _payment.amount);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,88 +1,95 @@
|
|||
pragma solidity >=0.5.0 <0.7.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "./KeycardWallet.sol";
|
||||
import "./KeycardRegistry.sol";
|
||||
|
||||
contract KeycardWalletFactory is KeycardRegistry {
|
||||
mapping(address => address[]) public ownersWallets;
|
||||
mapping(address => address) public keycardsWallets;
|
||||
address public currency;
|
||||
|
||||
event NewWallet(KeycardWallet wallet);
|
||||
|
||||
constructor(address _currency) public {
|
||||
currency = _currency;
|
||||
}
|
||||
|
||||
function create(address _keycard, bool _keycardIsOwner, uint256 _minBlockDistance, uint256 _txMaxAmount) public {
|
||||
address owner = _keycardIsOwner ? _keycard : msg.sender;
|
||||
|
||||
require(keycardsWallets[_keycard] == address(0), "the keycard is already associated to a wallet");
|
||||
|
||||
KeycardWallet wallet = new KeycardWallet(owner, _keycard, address(this), _minBlockDistance, currency, _txMaxAmount);
|
||||
ownersWallets[owner].push(address(wallet));
|
||||
keycardsWallets[_keycard] = address(wallet);
|
||||
emit NewWallet(wallet);
|
||||
}
|
||||
|
||||
function addressFind(address[] storage _arr, address _a) internal view returns (uint) {
|
||||
for (uint i = 0; i < _arr.length; i++){
|
||||
if (_arr[i] == _a) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
revert("address not found");
|
||||
}
|
||||
|
||||
function addressDelete(address[] storage _arr, uint _idx) internal {
|
||||
_arr[_idx] = _arr[_arr.length-1];
|
||||
_arr.length--;
|
||||
}
|
||||
|
||||
function setOwner(address _oldOwner, address _newOwner) public {
|
||||
uint idx = addressFind(ownersWallets[_oldOwner], msg.sender);
|
||||
ownersWallets[_newOwner].push(ownersWallets[_oldOwner][idx]);
|
||||
addressDelete(ownersWallets[_oldOwner], idx);
|
||||
}
|
||||
|
||||
function setKeycard(address _oldKeycard, address _newKeycard) public {
|
||||
address wallet = keycardsWallets[_oldKeycard];
|
||||
|
||||
require(wallet == msg.sender, "only the registered wallet can call this");
|
||||
require(keycardsWallets[_newKeycard] == address(0), "the keycard already has a wallet");
|
||||
|
||||
keycardsWallets[_newKeycard] = wallet;
|
||||
delete keycardsWallets[_oldKeycard];
|
||||
}
|
||||
|
||||
function unregisterFromOwner(address _wallet, address _keycard) public {
|
||||
uint idx = addressFind(ownersWallets[msg.sender], _wallet);
|
||||
|
||||
require(_wallet == keycardsWallets[_keycard], "owner required");
|
||||
|
||||
addressDelete(ownersWallets[msg.sender], idx);
|
||||
delete keycardsWallets[_keycard];
|
||||
}
|
||||
|
||||
function countWalletsForOwner(address owner) public view returns (uint) {
|
||||
return ownersWallets[owner].length;
|
||||
}
|
||||
|
||||
function unregister(address _owner, address _keycard) public {
|
||||
uint idx = addressFind(ownersWallets[_owner], msg.sender);
|
||||
|
||||
require(ownersWallets[_owner][idx] == keycardsWallets[_keycard], "only the associated keycard can be deassociated");
|
||||
|
||||
addressDelete(ownersWallets[_owner], idx);
|
||||
delete keycardsWallets[_keycard];
|
||||
}
|
||||
|
||||
function register(address _owner, address _keycard) public {
|
||||
require(keycardsWallets[_keycard] == address(0), "the keycard already has a wallet");
|
||||
|
||||
ownersWallets[_owner].push(msg.sender);
|
||||
keycardsWallets[_keycard] = msg.sender;
|
||||
}
|
||||
}
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "./KeycardWallet.sol";
|
||||
import "./KeycardRegistry.sol";
|
||||
|
||||
contract KeycardWalletFactory is KeycardRegistry {
|
||||
mapping(address => address[]) public ownersWallets;
|
||||
mapping(address => address) public keycardsWallets;
|
||||
address public currency;
|
||||
address public blockRelay;
|
||||
address public owner;
|
||||
|
||||
event NewWallet(KeycardWallet wallet);
|
||||
|
||||
function init(address _currency, address _blockRelay) public {
|
||||
require(owner == address(0), "this function can only be invoked once");
|
||||
|
||||
owner = msg.sender;
|
||||
currency = _currency;
|
||||
blockRelay = _blockRelay;
|
||||
}
|
||||
|
||||
function create(address _keycard, bool _keycardIsOwner, uint256 _minBlockDistance, uint256 _txMaxAmount) public {
|
||||
address owner = _keycardIsOwner ? _keycard : msg.sender;
|
||||
|
||||
require(keycardsWallets[_keycard] == address(0), "the keycard is already associated to a wallet");
|
||||
|
||||
KeycardWallet wallet = new KeycardWallet();
|
||||
ownersWallets[owner].push(address(wallet));
|
||||
keycardsWallets[_keycard] = address(wallet);
|
||||
wallet.init(owner, _keycard, address(this), blockRelay, _minBlockDistance, currency, _txMaxAmount);
|
||||
emit NewWallet(wallet);
|
||||
}
|
||||
|
||||
function addressFind(address[] storage _arr, address _a) internal view returns (uint) {
|
||||
for (uint i = 0; i < _arr.length; i++){
|
||||
if (_arr[i] == _a) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
revert("address not found");
|
||||
}
|
||||
|
||||
function addressDelete(address[] storage _arr, uint _idx) internal {
|
||||
_arr[_idx] = _arr[_arr.length-1];
|
||||
_arr.length--;
|
||||
}
|
||||
|
||||
function setOwner(address _oldOwner, address _newOwner) public {
|
||||
uint idx = addressFind(ownersWallets[_oldOwner], msg.sender);
|
||||
ownersWallets[_newOwner].push(ownersWallets[_oldOwner][idx]);
|
||||
addressDelete(ownersWallets[_oldOwner], idx);
|
||||
}
|
||||
|
||||
function setKeycard(address _oldKeycard, address _newKeycard) public {
|
||||
address wallet = keycardsWallets[_oldKeycard];
|
||||
|
||||
require(wallet == msg.sender, "only the registered wallet can call this");
|
||||
require(keycardsWallets[_newKeycard] == address(0), "the keycard already has a wallet");
|
||||
|
||||
keycardsWallets[_newKeycard] = wallet;
|
||||
delete keycardsWallets[_oldKeycard];
|
||||
}
|
||||
|
||||
function unregisterFromOwner(address _wallet, address _keycard) public {
|
||||
uint idx = addressFind(ownersWallets[msg.sender], _wallet);
|
||||
|
||||
require(_wallet == keycardsWallets[_keycard], "owner required");
|
||||
|
||||
addressDelete(ownersWallets[msg.sender], idx);
|
||||
delete keycardsWallets[_keycard];
|
||||
}
|
||||
|
||||
function countWalletsForOwner(address owner) public view returns (uint) {
|
||||
return ownersWallets[owner].length;
|
||||
}
|
||||
|
||||
function unregister(address _owner, address _keycard) public {
|
||||
uint idx = addressFind(ownersWallets[_owner], msg.sender);
|
||||
|
||||
require(ownersWallets[_owner][idx] == keycardsWallets[_keycard], "only the associated keycard can be deassociated");
|
||||
|
||||
addressDelete(ownersWallets[_owner], idx);
|
||||
delete keycardsWallets[_keycard];
|
||||
}
|
||||
|
||||
function register(address _owner, address _keycard) public {
|
||||
require(keycardsWallets[_keycard] == address(0), "the keycard already has a wallet");
|
||||
|
||||
ownersWallets[_owner].push(msg.sender);
|
||||
keycardsWallets[_keycard] = msg.sender;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
pragma solidity ^0.5.0;
|
||||
|
||||
contract MerchantsRegistry {
|
||||
address public owner;
|
||||
mapping(address => bool) public merchants;
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner, "owner required");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor() public {
|
||||
owner = msg.sender;
|
||||
}
|
||||
|
||||
function setOwner(address newOwner) public onlyOwner {
|
||||
owner = newOwner;
|
||||
}
|
||||
|
||||
function addMerchant(address merchantAddress) public onlyOwner {
|
||||
merchants[merchantAddress] = true;
|
||||
}
|
||||
|
||||
function removeMerchant(address merchantAddress) public onlyOwner {
|
||||
merchants[merchantAddress] = false;
|
||||
}
|
||||
}
|
||||
pragma solidity ^0.5.0;
|
||||
|
||||
contract MerchantsRegistry {
|
||||
address public owner;
|
||||
mapping(address => bool) public merchants;
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner, "owner required");
|
||||
_;
|
||||
}
|
||||
|
||||
function init() public {
|
||||
require(owner == address(0), "this function can only be invoked once");
|
||||
owner = msg.sender;
|
||||
}
|
||||
|
||||
function setOwner(address newOwner) public onlyOwner {
|
||||
owner = newOwner;
|
||||
}
|
||||
|
||||
function addMerchant(address merchantAddress) public onlyOwner {
|
||||
merchants[merchantAddress] = true;
|
||||
}
|
||||
|
||||
function removeMerchant(address merchantAddress) public onlyOwner {
|
||||
merchants[merchantAddress] = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
import "./IBlockRelay.sol";
|
||||
|
||||
contract MockBlockRelay is IBlockRelay {
|
||||
uint256 public lastBlock;
|
||||
mapping (uint256 => bytes32) public hashes;
|
||||
|
||||
function getNumber() public override view returns (uint256) {
|
||||
return lastBlock;
|
||||
}
|
||||
|
||||
function getHash(uint256 num) public override view returns (bytes32) {
|
||||
if ((num > lastBlock) || (lastBlock - num) > 255) {
|
||||
return bytes32(0);
|
||||
}
|
||||
|
||||
return hashes[num % 256];
|
||||
}
|
||||
|
||||
function addBlock(uint256 num, bytes32 h) external {
|
||||
require(num > lastBlock, "You can only add newer blocks");
|
||||
hashes[num % 255] = h;
|
||||
lastBlock = num;
|
||||
}
|
||||
}
|
|
@ -1,30 +1,29 @@
|
|||
pragma solidity >=0.5.0 <0.7.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "./IERC20.sol";
|
||||
|
||||
contract MockERC20 is IERC20 {
|
||||
function totalSupply() public view returns (uint256) {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
function balanceOf(address /*account*/) public view returns (uint256) {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
function transfer(address /*recipient*/, uint256 amount) public returns (bool) {
|
||||
return (amount <= 1000);
|
||||
}
|
||||
|
||||
function allowance(address /*owner*/, address /*spender*/) public view returns (uint256) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function approve(address /*spender*/, uint256 /*amount*/) public returns (bool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function transferFrom(address /*sender*/, address /*recipient*/, uint256 /*amount*/) public returns (bool) {
|
||||
return false;
|
||||
}
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
import "./IERC20.sol";
|
||||
|
||||
contract MockERC20 is IERC20 {
|
||||
function init() public {
|
||||
totalSupply = 1000;
|
||||
}
|
||||
|
||||
function balanceOf(address /*account*/) public view returns (uint256) {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
function transfer(address /*recipient*/, uint256 amount) public returns (bool) {
|
||||
return (amount <= 1000);
|
||||
}
|
||||
|
||||
function allowance(address /*owner*/, address /*spender*/) public view returns (uint256) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function approve(address /*spender*/, uint256 /*amount*/) public returns (bool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function transferFrom(address /*sender*/, address /*recipient*/, uint256 /*amount*/) public returns (bool) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
build/
|
||||
installed_contracts/
|
||||
node_modules/
|
|
@ -0,0 +1,41 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
import "./IBlockRelay.sol";
|
||||
|
||||
contract BlockRelay is IBlockRelay {
|
||||
uint256 public lastBlock;
|
||||
mapping (uint256 => bytes32) public hashes;
|
||||
address owner;
|
||||
|
||||
uint256 constant HISTORY_SIZE = 50;
|
||||
|
||||
function init(uint256 _firstBlock, bytes32 _firstHash) public {
|
||||
require(owner == address(0), "already done");
|
||||
owner = msg.sender;
|
||||
addBlock(_firstBlock, _firstHash);
|
||||
}
|
||||
|
||||
function getLast() public view returns (uint256) {
|
||||
return lastBlock;
|
||||
}
|
||||
|
||||
function getHash(uint256 num) public view returns (bytes32) {
|
||||
if ((num > lastBlock) || (lastBlock - num) >= HISTORY_SIZE) {
|
||||
return bytes32(0);
|
||||
}
|
||||
|
||||
return hashes[num % HISTORY_SIZE];
|
||||
}
|
||||
|
||||
function historySize() external view returns (uint256) {
|
||||
return HISTORY_SIZE;
|
||||
}
|
||||
|
||||
function addBlock(uint256 num, bytes32 h) public {
|
||||
require(num > lastBlock, "You can only add newer blocks");
|
||||
|
||||
hashes[num % HISTORY_SIZE] = h;
|
||||
lastBlock = num;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
import "./IERC20.sol";
|
||||
|
||||
contract ERC20 is IERC20 {
|
||||
uint256 constant private MAX_UINT256 = 2**256 - 1;
|
||||
mapping (address => uint256) public balances;
|
||||
mapping (address => mapping (address => uint256)) public allowed;
|
||||
|
||||
uint256 public totalSupply;
|
||||
|
||||
function init(uint256 _initialAmount) public {
|
||||
require(totalSupply == 0, "already done");
|
||||
|
||||
balances[msg.sender] = _initialAmount;
|
||||
totalSupply = _initialAmount;
|
||||
}
|
||||
|
||||
function transfer(address _to, uint256 _value) public returns (bool success) {
|
||||
require(balances[msg.sender] >= _value, "balance exceeded");
|
||||
balances[msg.sender] -= _value;
|
||||
balances[_to] += _value;
|
||||
emit Transfer(msg.sender, _to, _value);
|
||||
return true;
|
||||
}
|
||||
|
||||
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
|
||||
uint256 allowance = allowed[_from][msg.sender];
|
||||
require(balances[_from] >= _value && allowance >= _value, "balance or allowance exceeded");
|
||||
balances[_to] += _value;
|
||||
balances[_from] -= _value;
|
||||
if (allowance < MAX_UINT256) {
|
||||
allowed[_from][msg.sender] -= _value;
|
||||
}
|
||||
emit Transfer(_from, _to, _value);
|
||||
return true;
|
||||
}
|
||||
|
||||
function balanceOf(address _owner) public view returns (uint256 balance) {
|
||||
return balances[_owner];
|
||||
}
|
||||
|
||||
function approve(address _spender, uint256 _value) public returns (bool success) {
|
||||
allowed[msg.sender][_spender] = _value;
|
||||
emit Approval(msg.sender, _spender, _value);
|
||||
return true;
|
||||
}
|
||||
|
||||
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
|
||||
return allowed[_owner][_spender];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
import "./ERC20.sol";
|
||||
|
||||
contract ERC20Detailed is ERC20 {
|
||||
string private _name;
|
||||
string private _symbol;
|
||||
uint8 private _decimals;
|
||||
|
||||
/**
|
||||
* @dev Sets the values for `name`, `symbol`, and `decimals`. All three of
|
||||
* these values are immutable: they can only be set once during
|
||||
* construction.
|
||||
*/
|
||||
constructor (string memory name, string memory symbol, uint8 decimals) public {
|
||||
_name = name;
|
||||
_symbol = symbol;
|
||||
_decimals = decimals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the name of the token.
|
||||
*/
|
||||
function name() public view returns (string memory) {
|
||||
return _name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the symbol of the token, usually a shorter version of the
|
||||
* name.
|
||||
*/
|
||||
function symbol() public view returns (string memory) {
|
||||
return _symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the number of decimals used to get its user representation.
|
||||
* For example, if `decimals` equals `2`, a balance of `505` tokens should
|
||||
* be displayed to a user as `5,05` (`505 / 10 ** 2`).
|
||||
*
|
||||
* Tokens usually opt for a value of 18, imitating the relationship between
|
||||
* Ether and Wei.
|
||||
*
|
||||
* NOTE: This information is only used for _display_ purposes: it in
|
||||
* no way affects any of the arithmetic of the contract, including
|
||||
* {IERC20-balanceOf} and {IERC20-transfer}.
|
||||
*/
|
||||
function decimals() public view returns (uint8) {
|
||||
return _decimals;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
library EVMUtils {
|
||||
function eip712Hash(bytes32 _domainSeparator, bytes32 _messageHash) internal pure returns (bytes32) {
|
||||
return keccak256(abi.encodePacked(
|
||||
"\x19\x01",
|
||||
_domainSeparator,
|
||||
_messageHash
|
||||
));
|
||||
}
|
||||
|
||||
function recoverSigner(bytes32 _digest, bytes memory _sig) internal pure returns (address) {
|
||||
require(_sig.length == 65, "bad signature length");
|
||||
|
||||
bytes32 r;
|
||||
bytes32 s;
|
||||
uint8 v;
|
||||
|
||||
// solium-disable-next-line security/no-inline-assembly
|
||||
assembly {
|
||||
r := mload(add(_sig, 32))
|
||||
s := mload(add(_sig, 64))
|
||||
v := byte(0, mload(add(_sig, 96)))
|
||||
}
|
||||
|
||||
if (v < 27) {
|
||||
v += 27;
|
||||
}
|
||||
|
||||
require(v == 27 || v == 28, "signature version doesn't match");
|
||||
return ecrecover(_digest, v, r, s);
|
||||
}
|
||||
|
||||
function getChainID() internal pure returns (uint256) {
|
||||
uint256 id;
|
||||
assembly {
|
||||
id := chainid()
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
interface IBlockRelay {
|
||||
function getLast() external view returns (uint256);
|
||||
function getHash(uint256 /*blockNum*/) external view returns (bytes32);
|
||||
function historySize() external view returns (uint256);
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
|
||||
/**
|
||||
* @dev Interface of the ERC20 standard as defined in the EIP. Does not include
|
||||
* the optional functions; to access them see {ERC20Detailed}.
|
||||
*/
|
||||
interface IERC20 {
|
||||
/**
|
||||
* @dev Returns the amount of tokens in existence.
|
||||
*/
|
||||
function totalSupply() external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @dev Returns the amount of tokens owned by `account`.
|
||||
*/
|
||||
function balanceOf(address account) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @dev Moves `amount` tokens from the caller's account to `recipient`.
|
||||
*
|
||||
* Returns a boolean value indicating whether the operation succeeded.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function transfer(address recipient, uint256 amount) external returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Returns the remaining number of tokens that `spender` will be
|
||||
* allowed to spend on behalf of `owner` through {transferFrom}. This is
|
||||
* zero by default.
|
||||
*
|
||||
* This value changes when {approve} or {transferFrom} are called.
|
||||
*/
|
||||
function allowance(address owner, address spender) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
|
||||
*
|
||||
* Returns a boolean value indicating whether the operation succeeded.
|
||||
*
|
||||
* IMPORTANT: Beware that changing an allowance with this method brings the risk
|
||||
* that someone may use both the old and the new allowance by unfortunate
|
||||
* transaction ordering. One possible solution to mitigate this race
|
||||
* condition is to first reduce the spender's allowance to 0 and set the
|
||||
* desired value afterwards:
|
||||
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
|
||||
*
|
||||
* Emits an {Approval} event.
|
||||
*/
|
||||
function approve(address spender, uint256 amount) external returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Moves `amount` tokens from `sender` to `recipient` using the
|
||||
* allowance mechanism. `amount` is then deducted from the caller's
|
||||
* allowance.
|
||||
*
|
||||
* Returns a boolean value indicating whether the operation succeeded.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
|
||||
|
||||
/**
|
||||
* @dev Emitted when `value` tokens are moved from one account (`from`) to
|
||||
* another (`to`).
|
||||
*
|
||||
* Note that `value` may be zero.
|
||||
*/
|
||||
event Transfer(address indexed from, address indexed to, uint256 value);
|
||||
|
||||
/**
|
||||
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
|
||||
* a call to {approve}. `value` is the new allowance.
|
||||
*/
|
||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.4.21 <0.7.0;
|
||||
|
||||
contract Migrations {
|
||||
address public owner;
|
||||
uint public last_completed_migration;
|
||||
|
||||
constructor() public {
|
||||
owner = msg.sender;
|
||||
}
|
||||
|
||||
modifier restricted() {
|
||||
if (msg.sender == owner) _;
|
||||
}
|
||||
|
||||
function setCompleted(uint completed) public restricted {
|
||||
last_completed_migration = completed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.5.0 <0.7.0;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "./IERC20.sol";
|
||||
import "./IBlockRelay.sol";
|
||||
import "./EVMUtils.sol";
|
||||
|
||||
contract StatusPay {
|
||||
event NewPayment(address to, uint256 amount);
|
||||
|
||||
struct Payment {
|
||||
uint256 blockNumber;
|
||||
bytes32 blockHash;
|
||||
uint256 amount;
|
||||
address to;
|
||||
}
|
||||
|
||||
struct Account {
|
||||
bool exists;
|
||||
uint256 balance;
|
||||
uint256 lastUsedBlock;
|
||||
uint256 minBlockDistance;
|
||||
uint256 maxTxAmount;
|
||||
}
|
||||
|
||||
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
||||
bytes32 constant PAYMENT_TYPEHASH = keccak256("Payment(uint256 blockNumber,bytes32 blockHash,uint256 amount,address to)");
|
||||
bytes32 DOMAIN_SEPARATOR;
|
||||
|
||||
uint256 public maxTxDelayInBlocks;
|
||||
IBlockRelay public blockRelay;
|
||||
IERC20 public token;
|
||||
address public networkOwner;
|
||||
|
||||
mapping(address => address) public keycards;
|
||||
mapping(address => Account) public accounts;
|
||||
|
||||
function init(address _blockRelay, address _token, uint256 _maxDelayInBlocks) public {
|
||||
require(networkOwner == address(0), "already done");
|
||||
|
||||
networkOwner = msg.sender;
|
||||
blockRelay = IBlockRelay(_blockRelay);
|
||||
token = IERC20(_token);
|
||||
|
||||
require(_maxDelayInBlocks <= blockRelay.historySize(), "max delay cannot be more than history size");
|
||||
|
||||
maxTxDelayInBlocks = _maxDelayInBlocks;
|
||||
|
||||
DOMAIN_SEPARATOR = keccak256(abi.encode(
|
||||
EIP712DOMAIN_TYPEHASH,
|
||||
keccak256("StatusPay"),
|
||||
keccak256("1"),
|
||||
EVMUtils.getChainID(),
|
||||
address(this)
|
||||
));
|
||||
}
|
||||
|
||||
function createAccount(address _owner, address _keycard, uint256 _minBlockDistance, uint256 _maxTxAmount) public {
|
||||
require(networkOwner == msg.sender, "only the network owner can create accounts");
|
||||
|
||||
Account storage account = accounts[_owner];
|
||||
require(!account.exists, "already exists");
|
||||
|
||||
if (_keycard != address(0)) {
|
||||
_addKeycard(_keycard, _owner);
|
||||
}
|
||||
|
||||
account.exists = true;
|
||||
account.lastUsedBlock = blockRelay.getLast();
|
||||
account.minBlockDistance = _minBlockDistance;
|
||||
account.maxTxAmount = _maxTxAmount;
|
||||
}
|
||||
|
||||
function transferAccount(address _newOwner, address _keycard) public {
|
||||
Account storage oldAcc = accounts[msg.sender];
|
||||
require(oldAcc.exists, "account to transfer does not exist");
|
||||
|
||||
Account storage newAcc = accounts[_newOwner];
|
||||
require(!newAcc.exists, "the new owner already has an account");
|
||||
|
||||
newAcc.exists = true;
|
||||
newAcc.balance = oldAcc.balance;
|
||||
newAcc.lastUsedBlock = oldAcc.lastUsedBlock;
|
||||
newAcc.minBlockDistance = oldAcc.minBlockDistance;
|
||||
newAcc.maxTxAmount = oldAcc.maxTxAmount;
|
||||
|
||||
oldAcc.exists = false;
|
||||
oldAcc.balance = 0;
|
||||
oldAcc.lastUsedBlock = 0;
|
||||
oldAcc.minBlockDistance = 0;
|
||||
oldAcc.maxTxAmount = 0;
|
||||
|
||||
if (_keycard != address(0)) {
|
||||
_addKeycard(_keycard, _newOwner);
|
||||
}
|
||||
}
|
||||
|
||||
function addKeycard(address _keycard) public {
|
||||
_addKeycard(_keycard, msg.sender);
|
||||
}
|
||||
|
||||
function _addKeycard(address _keycard, address _owner) internal {
|
||||
require(!accounts[keycards[_keycard]].exists, "keycard already assigned");
|
||||
keycards[_keycard] = _owner;
|
||||
}
|
||||
|
||||
function removeKeycard(address _keycard) public {
|
||||
require(keycards[_keycard] == msg.sender, "keycard not owned");
|
||||
keycards[_keycard] = address(0);
|
||||
}
|
||||
|
||||
function topup(address _to, uint256 _amount) public {
|
||||
Account storage topped = accounts[_to];
|
||||
require(topped.exists, "account does not exist");
|
||||
require(token.transferFrom(msg.sender, address(this), _amount), "transfer failed");
|
||||
|
||||
topped.balance += _amount;
|
||||
}
|
||||
|
||||
function withdraw(address _to, uint256 _amount) public {
|
||||
Account storage exiting = accounts[msg.sender];
|
||||
require(exiting.exists, "account does not exist");
|
||||
require(exiting.balance >= _amount, "not enough balance");
|
||||
|
||||
exiting.balance -= _amount;
|
||||
require(token.transfer(_to, _amount), "transfer failed");
|
||||
}
|
||||
|
||||
function requestPayment(Payment memory _payment, bytes memory _signature) public {
|
||||
address signer = EVMUtils.recoverSigner(EVMUtils.eip712Hash(DOMAIN_SEPARATOR, hash(_payment)), _signature);
|
||||
Account storage payer = accounts[keycards[signer]];
|
||||
|
||||
// allow direct payment without Keycard from owner
|
||||
if (!payer.exists) {
|
||||
payer = accounts[signer];
|
||||
}
|
||||
|
||||
// check that a keycard is associated to this account
|
||||
require(payer.exists, "no account for this Keycard");
|
||||
|
||||
// check that the payee exists
|
||||
Account storage payee = accounts[_payment.to];
|
||||
require(payee.exists, "payee account does not exist");
|
||||
|
||||
// check that _payment.amount is not greater than the maxTxValue for this currency
|
||||
require(_payment.amount <= payer.maxTxAmount, "amount not allowed");
|
||||
|
||||
// check that balance is enough for this payment
|
||||
require(payer.balance >= _payment.amount, "balance is not enough");
|
||||
|
||||
uint256 blockNumber = blockRelay.getLast();
|
||||
|
||||
// check that the block number used for signing is not newer than the block number
|
||||
require(_payment.blockNumber <= blockNumber, "transaction cannot be in the future");
|
||||
|
||||
// check that the block number used is not too old
|
||||
require(_payment.blockNumber > (blockNumber - maxTxDelayInBlocks), "transaction too old");
|
||||
|
||||
// check that the block number is not too near to the last one in which a tx has been processed
|
||||
require(_payment.blockNumber >= (payer.lastUsedBlock + payer.minBlockDistance), "cooldown period not expired yet");
|
||||
|
||||
// check that the blockHash is valid
|
||||
require(_payment.blockHash == blockRelay.getHash(_payment.blockNumber), "invalid block hash");
|
||||
// this check is redundant but provideds a safety net if the oracle returns a 0 hash
|
||||
require(_payment.blockHash != bytes32(0), "invalid block hash");
|
||||
|
||||
// perform transfer
|
||||
payer.balance -= _payment.amount;
|
||||
payee.balance += _payment.amount;
|
||||
|
||||
// set new baseline block for checks
|
||||
payer.lastUsedBlock = blockNumber;
|
||||
emit NewPayment(_payment.to, _payment.amount);
|
||||
}
|
||||
|
||||
function hash(Payment memory _payment) internal pure returns (bytes32) {
|
||||
return keccak256(abi.encode(
|
||||
PAYMENT_TYPEHASH,
|
||||
_payment.blockNumber,
|
||||
_payment.blockHash,
|
||||
_payment.amount,
|
||||
_payment.to
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
const Migrations = artifacts.require("Migrations");
|
||||
|
||||
module.exports = function(deployer) {
|
||||
deployer.deploy(Migrations);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "status-pay",
|
||||
"version": "0.0.1",
|
||||
"description": "Status Payment Network",
|
||||
"scripts": {
|
||||
"clean": "rimraf build",
|
||||
"test": "truffle test",
|
||||
"compile": "truffle compile",
|
||||
"build:ovm": "DEBUG=info*,warn*,error* truffle compile --config truffle-config-ovm.js",
|
||||
"test:ovm": "DEBUG=info*,warn* truffle test --config truffle-config-ovm.js",
|
||||
"all:ovm": "yarn clean && yarn build:ovm && yarn test:ovm"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eth-optimism/ovm-truffle-provider-wrapper": "^0.0.1-alpha.27",
|
||||
"@eth-optimism/rollup-full-node": "^0.0.1-alpha.29",
|
||||
"@eth-optimism/solc": "^0.5.16-alpha.0",
|
||||
"solc": "0.5.16",
|
||||
"eth-sig-util": "^2.5.3",
|
||||
"ethers": "^5.0.7",
|
||||
"minimist": "^1.2.5",
|
||||
"truffle": "^5.1.34",
|
||||
"truffle-hdwallet-provider": "^1.0.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bip39": "^3.0.2",
|
||||
"ethereumjs-wallet": "^1.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
const fs = require('fs');
|
||||
const { ethers } = require("ethers");
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
|
||||
const NUTBERRY_TX_TYPED_DATA = {
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'version', type: 'string' },
|
||||
],
|
||||
Transaction: [
|
||||
{ name: 'to', type: 'address' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'data', type: 'bytes' },
|
||||
],
|
||||
},
|
||||
primaryType: 'Transaction',
|
||||
domain: {
|
||||
name: 'NutBerry',
|
||||
version: '2',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = class Account {
|
||||
constructor(rpcURL) {
|
||||
this.provider = new ethers.providers.JsonRpcProvider(rpcURL);
|
||||
this.sender = null;
|
||||
this.senderAddress = null;
|
||||
}
|
||||
|
||||
async init(account, passfile) {
|
||||
this.sender = await this.loadAccount(account, passfile);
|
||||
this.senderAddress = await this.sender.getAddress();
|
||||
}
|
||||
|
||||
async loadAccount(account, passfile) {
|
||||
let json = fs.readFileSync(account, "utf-8");
|
||||
let pass = fs.readFileSync(passfile, "utf-8").split("\n")[0].replace("\r", "");
|
||||
return await ethers.Wallet.fromEncryptedJson(json, pass);
|
||||
}
|
||||
|
||||
async sendDataTx(to, data) {
|
||||
let signedTx = this.signTransaction({
|
||||
to: to,
|
||||
data: data,
|
||||
nonce: await this.provider.getTransactionCount(this.senderAddress, 'pending')
|
||||
});
|
||||
|
||||
let txHash = await this.provider.send('eth_sendRawTransaction', [signedTx]);
|
||||
return await this.provider.getTransactionReceipt(txHash);
|
||||
}
|
||||
|
||||
encodeTx(tx) {
|
||||
function arrayify (val) {
|
||||
let v = val;
|
||||
|
||||
if (typeof v === 'number' || typeof v === 'bigint') {
|
||||
v = v.toString(16);
|
||||
if (v.length % 2) {
|
||||
v = `0x0${v}`;
|
||||
} else {
|
||||
v = `0x${v}`;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(ethers.utils.arrayify(v));
|
||||
}
|
||||
|
||||
const nonceBytes = arrayify(tx.nonce);
|
||||
const calldataBytes = arrayify(tx.data);
|
||||
let enc = arrayify(tx.v)
|
||||
.concat(arrayify(tx.r))
|
||||
.concat(arrayify(tx.s));
|
||||
|
||||
if (nonceBytes.length > 1 || nonceBytes[0] > 0xde) {
|
||||
enc.push(0xff - nonceBytes.length);
|
||||
enc = enc.concat(nonceBytes);
|
||||
} else {
|
||||
enc = enc.concat(nonceBytes);
|
||||
}
|
||||
|
||||
enc = enc.concat(arrayify(tx.to));
|
||||
|
||||
if (calldataBytes.length >= 0xff) {
|
||||
enc.push(0xff);
|
||||
enc.push(calldataBytes.length >> 8);
|
||||
enc.push(calldataBytes.length & 0xff);
|
||||
} else {
|
||||
enc.push(calldataBytes.length);
|
||||
}
|
||||
|
||||
return ethers.utils.hexlify(enc.concat(calldataBytes));
|
||||
}
|
||||
|
||||
signTransaction(tx) {
|
||||
const sig = this.signTypedData(tx, NUTBERRY_TX_TYPED_DATA);
|
||||
const { r, s, v } = ethers.utils.splitSignature(sig);
|
||||
|
||||
return this.encodeTx(Object.assign(tx, { r, s, v: v + 101 }));
|
||||
}
|
||||
|
||||
signTypedData(message, typeInfo) {
|
||||
const obj = Object.assign({ message: message }, typeInfo);
|
||||
return ethSigUtil.signTypedData(Buffer.from(this.sender.privateKey.substring(2), "hex"), { data: obj });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
const Account = require('./account.js');
|
||||
const { ethers } = require("ethers");
|
||||
|
||||
const utils = require('./utils.js');
|
||||
const parseArgs = require('minimist');
|
||||
const BRIDGE_ADDRESS = '0xa9f96d8761aa05430d3ee2e7dff6b04978f13369';
|
||||
const RPC_URL = `https://${BRIDGE_ADDRESS}.fly.dev`;
|
||||
|
||||
const argv = parseArgs(process.argv.slice(2), {string: ["token", "wallet", "blockrelay", "statuspay", "keycard", "merchant", "blockHash"], default: {"endpoint": RPC_URL, "token": "0x722dd3f80bac40c951b51bdd28dd19d435762180", maxTxDelayInBlocks: 10, minBlockDistance: 1, maxTxAmount: 1000000000000000000}});
|
||||
|
||||
const PAYMENT_TYPED_DATA = {
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{ name: "name", type: "string" },
|
||||
{ name: "version", type: "string" },
|
||||
{ name: "chainId", type: "uint256" },
|
||||
{ name: "verifyingContract", type: "address" }
|
||||
],
|
||||
Payment: [
|
||||
{ name: "blockNumber", type: "uint256" },
|
||||
{ name: "blockHash", type: "bytes32" },
|
||||
{ name: "amount", type: "uint256" },
|
||||
{ name: "to", type: "address" }
|
||||
]
|
||||
},
|
||||
primaryType: "Payment",
|
||||
domain: {
|
||||
name: "StatusPay",
|
||||
version: "1",
|
||||
chainId: 3,
|
||||
verifyingContract: statusPay.address
|
||||
}
|
||||
};
|
||||
|
||||
async function loadSigner(argv, signer, passfile) {
|
||||
let account = new Account(argv["endpoint"]);
|
||||
|
||||
if (argv[signer]) {
|
||||
if (!argv[passfile]) {
|
||||
console.error(`the --${passfile} option must be specified`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await account.init(argv[signer], argv[passfile]);
|
||||
} else if (argv["cmd"] != "info") {
|
||||
console.error(`--${signer} is required`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
function getContract(argv, signer, optName, contractName) {
|
||||
if (!argv[optName]) {
|
||||
console.error(`the --${optName} option must be specified`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return utils.loadContract(argv[optName], contractName, signer.provider);
|
||||
}
|
||||
|
||||
function getBlockRelayContract(argv, signer) {
|
||||
return getContract(argv, signer, "blockrelay", "BlockRelay");
|
||||
}
|
||||
|
||||
function getERC20Contract(argv, signer) {
|
||||
return getContract(argv, signer, "token", "ERC20Detailed");
|
||||
}
|
||||
|
||||
function getStatusPayContract(argv, signer) {
|
||||
return getContract(argv, signer, "statuspay", "StatusPay");
|
||||
}
|
||||
|
||||
function getMandatory(argv, opt) {
|
||||
if (!argv[opt]) {
|
||||
console.error(`the --${opt} option must be specified`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return argv[opt];
|
||||
}
|
||||
|
||||
function getBlockOptions(argv) {
|
||||
if (!argv["blockHash"] || !argv["blockNumber"]) {
|
||||
console.error(`the --blockHash and --blockNumber options must be specified`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {blockNumber: argv["blockNumber"], blockHash: argv["blockHash"]};
|
||||
}
|
||||
|
||||
async function initBlockRelay(argv, signer) {
|
||||
let blockRelay = getBlockRelayContract(argv, signer);
|
||||
let opts = getBlockOptions(argv);
|
||||
await signer.sendDataTx(blockRelay.address, blockRelay.interface.encodeFunctionData("init", [opts.blockNumber, opts.blockHash]));
|
||||
}
|
||||
|
||||
async function addBlock(argv, signer) {
|
||||
let blockRelay = getBlockRelayContract(argv, signer);
|
||||
let opts = getBlockOptions(argv);
|
||||
|
||||
await signer.sendDataTx(blockRelay.address, blockRelay.interface.encodeFunctionData("addBlock", [opts.blockNumber, opts.blockHash]));
|
||||
}
|
||||
|
||||
async function initStatusPay(argv, signer) {
|
||||
let blockRelay = getBlockRelayContract(argv, signer);
|
||||
let erc20 = getERC20Contract(argv, signer);
|
||||
let statusPay = getStatusPayContract(argv, signer);
|
||||
let maxTxDelayInBlocks = argv["maxTxDelayInBlocks"];
|
||||
|
||||
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("init", [blockRelay.address, erc20.address, maxTxDelayInBlocks]));
|
||||
}
|
||||
|
||||
async function createWallet(argv, signer) {
|
||||
let wallet = getMandatory(argv, "wallet");
|
||||
let statusPay = getStatusPayContract(argv, signer);
|
||||
let keycard = argv["keycard"] || ethers.constants.AddressZero;
|
||||
|
||||
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("createAccount", [wallet, keycard, argv["minBlockDistance"], argv["maxTxAmount"]]));
|
||||
}
|
||||
|
||||
async function topup(argv, signer) {
|
||||
let wallet = getMandatory(argv, "wallet");
|
||||
let amount = getMandatory(argv, "amount");
|
||||
let statusPay = getStatusPayContract(argv, signer);
|
||||
|
||||
argv["token"] = await statusPay.token();
|
||||
let erc20 = getERC20Contract(argv, signer);
|
||||
|
||||
await signer.sendDataTx(erc20.address, erc20.interface.encodeFunctionData("approve", [statusPay.address, amount]));
|
||||
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("topup", [wallet, amount]));
|
||||
}
|
||||
|
||||
async function withdraw(argv, signer) {
|
||||
let wallet = getMandatory(argv, "wallet");
|
||||
let amount = getMandatory(argv, "amount");
|
||||
let statusPay = getStatusPayContract(argv, signer);
|
||||
|
||||
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("withdraw", [wallet, amount]));
|
||||
}
|
||||
|
||||
async function payment(argv, signer) {
|
||||
let keycardSigner = await loadSigner(argv, "keycardsigner", "keycardpassfile");
|
||||
let statusPay = getStatusPayContract(argv, signer);
|
||||
let merchant = getMandatory(argv, "merchant");
|
||||
let amount = getMandatory(argv, "amount");
|
||||
|
||||
argv["blockrelay"] = await statusPay.blockRelay();
|
||||
let blockRelay = getBlockRelayContract(argv, signer);
|
||||
let blockNumber = await blockRelay.getLast();
|
||||
let blockHash = await blockRelay.getHash(blockNumber);
|
||||
|
||||
let message = {blockNumber: blockNumber, blockHash: blockHash, amount: amount, to: merchant};
|
||||
let sig = keycardSigner.signTypedData(message, PAYMENT_TYPED_DATA);
|
||||
|
||||
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("requestPayment", [message, sig]));
|
||||
}
|
||||
|
||||
async function info(argv, signer) {
|
||||
let statusPay = getStatusPayContract(argv, signer);
|
||||
|
||||
console.log(`StatusPay (${statusPay.address})`);
|
||||
console.log("==");
|
||||
let networkOwner = await statusPay.networkOwner();
|
||||
if (networkOwner == ethers.constants.AddressZero) {
|
||||
console.log("This StatusPay instance has not yet been initialized");
|
||||
} else {
|
||||
let additionalInfo = ((signer.senderAddress != null) && (signer.senderAddress.toLowerCase() == networkOwner.toLowerCase())) ? " (you)" : "";
|
||||
console.log(`Network owner: ${networkOwner}${additionalInfo}`);
|
||||
}
|
||||
|
||||
argv["token"] = await statusPay.token();
|
||||
let erc20 = getERC20Contract(argv, signer);
|
||||
|
||||
argv["blockrelay"] = await statusPay.blockRelay();
|
||||
let blockRelay = getBlockRelayContract(argv, signer);
|
||||
|
||||
let erc20Unit = await erc20.decimals();
|
||||
let ownedTokens = ethers.utils.formatUnits(await erc20.balanceOf(statusPay.address), erc20Unit);
|
||||
|
||||
console.log(`Token: ${erc20.address}`);
|
||||
console.log(`Block Relay: ${blockRelay.address}`);
|
||||
console.log(`Max transaction delay in blocks: ${await statusPay.maxTxDelayInBlocks()}`);
|
||||
console.log(`Deposited token amount: ${ownedTokens}\n`);
|
||||
|
||||
if (argv["keycard"]) {
|
||||
argv["wallet"] = await statusPay.keycards(argv["keycard"]);
|
||||
if (argv["wallet"] == ethers.constants.AddressZero) {
|
||||
console.log(`Keycard ${argv["keycard"]} has no associated account\n`);
|
||||
argv["wallet"] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (argv["wallet"]) {
|
||||
let account = await statusPay.accounts(argv["wallet"]);
|
||||
|
||||
if (!account.exists) {
|
||||
let additionalInfo = argv["keycard"] ? `, but Keycard ${argv["keycard"]} is associated to it` : "";
|
||||
console.log(`Account ${argv["wallet"]} does not exist${additionalInfo}\n`);
|
||||
} else {
|
||||
console.log(`Account (${argv["wallet"]})`);
|
||||
console.log("==");
|
||||
if (argv["keycard"]) {
|
||||
console.log(`Keycard (there can be others too): ${argv["keycard"]}`);
|
||||
}
|
||||
console.log(`Balance: ${ethers.utils.formatUnits(account.balance, erc20Unit)}`);
|
||||
console.log(`Last payment on block: ${account.lastUsedBlock}`);
|
||||
console.log(`Cool-off period in blocks: ${account.minBlockDistance}`);
|
||||
console.log(`Max transaction amount: ${ethers.utils.formatUnits(account.maxTxAmount, erc20Unit)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Token (${erc20.address})`);
|
||||
console.log("==");
|
||||
console.log(`Name: ${await erc20.name()}`);
|
||||
console.log(`Symbol: ${await erc20.symbol()}`);
|
||||
console.log(`Total supply: ${ethers.utils.formatUnits(await erc20.totalSupply(), erc20Unit)}\n`);
|
||||
|
||||
let lastBlock = await blockRelay.getLast();
|
||||
console.log(`BlockRelay (${blockRelay.address})`);
|
||||
console.log("==");
|
||||
console.log(`Last block: ${lastBlock}`);
|
||||
console.log(`Last hash: ${await blockRelay.getHash(lastBlock)}`);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
let signer = await loadSigner(argv, "signer", "passfile");
|
||||
|
||||
switch(argv["cmd"]) {
|
||||
case "init-block-relay":
|
||||
await initBlockRelay(argv, signer);
|
||||
break;
|
||||
case "add-block":
|
||||
await addBlock(argv, signer);
|
||||
break;
|
||||
case "init-status-pay":
|
||||
await initStatusPay(argv, signer);
|
||||
break;
|
||||
case "create-wallet":
|
||||
await createWallet(argv, signer);
|
||||
break;
|
||||
case "topup":
|
||||
await topup(argv, signer);
|
||||
break;
|
||||
case "withdraw":
|
||||
await withdraw(argv, signer);
|
||||
break;
|
||||
case "payment":
|
||||
await payment(argv, signer);
|
||||
break;
|
||||
case "info":
|
||||
await info(argv, signer);
|
||||
break;
|
||||
default:
|
||||
console.log("The --cmd option is mandatory. Possible commands: init-block-relay, add-block, init-status-pay, create-wallet, topup, withdraw, payment");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
run();
|
|
@ -0,0 +1,15 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { ethers } = require('ethers');
|
||||
const CONTRACTS_PATH="../build/contracts";
|
||||
|
||||
module.exports.loadContractFile = (fileName) => {
|
||||
let content = fs.readFileSync(path.join(__dirname, CONTRACTS_PATH, fileName), "utf-8");
|
||||
return content;
|
||||
};
|
||||
|
||||
module.exports.loadContract = (address, contractName, signerOrProvider) => {
|
||||
let content = this.loadContractFile(`${contractName}.json`);
|
||||
let contract = JSON.parse(content);
|
||||
return new ethers.Contract(address, contract.abi, signerOrProvider);
|
||||
};
|
|
@ -0,0 +1,243 @@
|
|||
const ERC20 = artifacts.require('ERC20');
|
||||
const BlockRelay = artifacts.require('BlockRelay');
|
||||
const StatusPay = artifacts.require('StatusPay');
|
||||
|
||||
const bip39 = require('bip39');
|
||||
const { hdkey } = require('ethereumjs-wallet');
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
|
||||
let token, block, statusPay, keycardKey;
|
||||
|
||||
const zeroAddress = "0x0000000000000000000000000000000000000000";
|
||||
const seed = bip39.mnemonicToSeedSync("candy maple cake sugar pudding cream honey rich smooth crumble sweet treat");
|
||||
const hdk = hdkey.fromMasterSeed(seed);
|
||||
|
||||
const CHAIN_ID = 1; //for now 1
|
||||
|
||||
contract('StatusPay', (accounts) => {
|
||||
const owner = accounts[0];
|
||||
const keycard = accounts[1];
|
||||
const merchant = accounts[2];
|
||||
const network = accounts[3];
|
||||
|
||||
before(async () => {
|
||||
keycardKey = deriveKey(1, keycard);
|
||||
|
||||
token = await ERC20.new({from: network});
|
||||
block = await BlockRelay.new({from: network});
|
||||
statusPay = await StatusPay.new({from: network});
|
||||
|
||||
await token.init(10000, {from: network});
|
||||
await block.init(500, "0xbababababaabaabaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", {from: network});
|
||||
await statusPay.init(block.address, token.address, 10, {from: network});
|
||||
|
||||
await token.transfer(owner, 100, {from: network});
|
||||
});
|
||||
|
||||
it('requestPayment with inexistant account', async () => {
|
||||
try {
|
||||
await requestPaymentTest(10);
|
||||
assert.fail("requestPayment should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "no account for this Keycard");
|
||||
}
|
||||
});
|
||||
|
||||
it('creates buyer account', async () => {
|
||||
await statusPay.createAccount(owner, keycard, 1, 10, {from: network});
|
||||
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 0);
|
||||
});
|
||||
|
||||
it('requestPayment with inexisting merchant', async () => {
|
||||
try {
|
||||
await requestPaymentTest(10);
|
||||
assert.fail("requestPayment should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "payee account does not exist");
|
||||
}
|
||||
});
|
||||
|
||||
it('creates merchant account', async () => {
|
||||
await statusPay.createAccount(merchant, zeroAddress, 1, 1000, {from: network});
|
||||
assert.equal((await statusPay.accounts.call(merchant)).balance.toNumber(), 0);
|
||||
});
|
||||
|
||||
it('requestPayment with insufficient balance', async () => {
|
||||
try {
|
||||
await requestPaymentTest(10);
|
||||
assert.fail("requestPayment should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "balance is not enough");
|
||||
}
|
||||
});
|
||||
|
||||
it('topup account', async () => {
|
||||
await token.approve(statusPay.address, 100, {from: owner});
|
||||
await statusPay.topup(owner, 100, {from: owner});
|
||||
await block.addBlock(501, "0xbababababaabaabaaaacaabaaaaaaadaaadcaaadaaaaaaacaaaaaaddeaaaaaaa", {from: network});
|
||||
|
||||
assert.equal((await token.balanceOf.call(owner)).toNumber(), 0);
|
||||
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 100);
|
||||
});
|
||||
|
||||
it('topup exceed ERC20 balance', async () => {
|
||||
await token.approve(statusPay.address, 100, {from: owner});
|
||||
|
||||
try {
|
||||
await statusPay.topup(owner, 100, {from: owner});
|
||||
assert.fail("topup should have failed");
|
||||
} catch (err) {
|
||||
assert(err.reason == "transfer failed" || err.reason == "balance or allowance exceeded");
|
||||
}
|
||||
|
||||
assert.equal((await token.balanceOf.call(owner)).toNumber(), 0);
|
||||
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 100);
|
||||
});
|
||||
|
||||
it('topup account non-existing account', async () => {
|
||||
try {
|
||||
await statusPay.topup(network, 100, {from: network});
|
||||
assert.fail("topup should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "account does not exist");
|
||||
}
|
||||
});
|
||||
|
||||
it('requestPayment over limit', async () => {
|
||||
try {
|
||||
await requestPaymentTest(11);
|
||||
assert.fail("requestPayment should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "amount not allowed");
|
||||
}
|
||||
});
|
||||
|
||||
it('requestPayment with block too old', async () => {
|
||||
try {
|
||||
await requestPaymentTest(10, (await block.getLast.call()).toNumber() - 10);
|
||||
assert.fail("requestPayment should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "transaction too old");
|
||||
}
|
||||
});
|
||||
|
||||
it('requestPayment with invalid hash', async () => {
|
||||
try {
|
||||
await requestPaymentTest(10, undefined, "0xbababababaabaabaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
assert.fail("requestPayment should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "invalid block hash");
|
||||
}
|
||||
});
|
||||
|
||||
it('requestPayment', async () => {
|
||||
const receipt = await requestPaymentTest(10);
|
||||
|
||||
const event = receipt.logs.find(element => element.event.match('NewPayment'));
|
||||
|
||||
assert.equal(event.args.to, merchant);
|
||||
assert.equal(event.args.amount, 10);
|
||||
|
||||
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 90);
|
||||
assert.equal((await statusPay.accounts.call(merchant)).balance.toNumber(), 10);
|
||||
});
|
||||
|
||||
it('requestPayment without waiting for cooldown', async () => {
|
||||
try {
|
||||
await requestPaymentTest(10);
|
||||
assert.fail("requestPayment should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "cooldown period not expired yet");
|
||||
}
|
||||
});
|
||||
|
||||
it('requestPayment with block in the future', async () => {
|
||||
try {
|
||||
await requestPaymentTest(10, (await block.getLast.call()).toNumber() + 1);
|
||||
assert.fail("requestPayment should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "transaction cannot be in the future");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('withdraw', async () => {
|
||||
await statusPay.withdraw(owner, 80, {from: owner});
|
||||
|
||||
assert.equal((await token.balanceOf.call(owner)).toNumber(), 80);
|
||||
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 10);
|
||||
});
|
||||
|
||||
it('withdraw more than balance allows', async () => {
|
||||
try {
|
||||
await statusPay.withdraw(owner, 11, {from: owner});
|
||||
assert.fail("withdraw should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "not enough balance");
|
||||
}
|
||||
|
||||
assert.equal((await token.balanceOf.call(owner)).toNumber(), 80);
|
||||
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 10);
|
||||
});
|
||||
|
||||
it('withdraw non existing account', async () => {
|
||||
try {
|
||||
await statusPay.withdraw(network, 10, {from: network});
|
||||
assert.fail("withdraw should have failed");
|
||||
} catch (err) {
|
||||
assert.equal(err.reason, "account does not exist");
|
||||
}
|
||||
});
|
||||
|
||||
requestPaymentTest = async (value, blockNum, blockH) => {
|
||||
const blockNumber = blockNum || (await block.getLast.call()).toNumber();
|
||||
const blockHash = blockH || await block.getHash.call(blockNumber);
|
||||
|
||||
const message = {blockNumber: blockNumber, blockHash: blockHash, amount: value, to: merchant};
|
||||
const sig = signPaymentRequest(keycardKey, message);
|
||||
return await statusPay.requestPayment(message, sig, {from: merchant});
|
||||
};
|
||||
|
||||
deriveKey = (index, expectedAddr) => {
|
||||
const addrNode = hdk.derivePath("m/44'/60'/0'/0/" + index);
|
||||
const generatedAddr = addrNode.getWallet().getAddressString();
|
||||
assert.equal(generatedAddr.toLowerCase(), expectedAddr.toLowerCase());
|
||||
|
||||
return addrNode.getWallet().getPrivateKey();
|
||||
};
|
||||
|
||||
signPaymentRequest = (signer, message) => {
|
||||
let domain = [
|
||||
{ name: "name", type: "string" },
|
||||
{ name: "version", type: "string" },
|
||||
{ name: "chainId", type: "uint256" },
|
||||
{ name: "verifyingContract", type: "address" }
|
||||
];
|
||||
|
||||
let payment = [
|
||||
{ name: "blockNumber", type: "uint256" },
|
||||
{ name: "blockHash", type: "bytes32" },
|
||||
{ name: "amount", type: "uint256" },
|
||||
{ name: "to", type: "address" }
|
||||
];
|
||||
|
||||
let domainData = {
|
||||
name: "StatusPay",
|
||||
version: "1",
|
||||
chainId: CHAIN_ID,
|
||||
verifyingContract: statusPay.address
|
||||
};
|
||||
|
||||
let data = {
|
||||
types: {
|
||||
EIP712Domain: domain,
|
||||
Payment: payment
|
||||
},
|
||||
primaryType: "Payment",
|
||||
domain: domainData,
|
||||
message: message
|
||||
};
|
||||
|
||||
return ethSigUtil.signTypedData(signer, { data: data });
|
||||
};
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
const HDWalletProvider = require("truffle-hdwallet-provider");
|
||||
const ProviderWrapper = require("@eth-optimism/ovm-truffle-provider-wrapper");
|
||||
const mnemonic = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat";
|
||||
|
||||
// Set this to the desired Execution Manager Address -- required for the transpiler
|
||||
process.env.EXECUTION_MANAGER_ADDRESS = process.env.EXECUTION_MANAGER_ADDRESS || "0x6454c9d69a4721feba60e26a367bd4d56196ee7c";
|
||||
const gasPrice = process.env.OVM_DEFAULT_GAS_PRICE || 0;
|
||||
const gas = process.env.OVM_DEFAULT_GAS || 1000000000;
|
||||
|
||||
|
||||
module.exports = {
|
||||
contracts_build_directory: './build/contracts',
|
||||
/**
|
||||
* Note: Using the `test` network will start a local node at 'http://127.0.0.1:8545/'
|
||||
*
|
||||
* To run tests:
|
||||
* $ truffle test ./truffle-tests/test-erc20.js --config truffle-config-ovm.js
|
||||
*/
|
||||
networks: {
|
||||
test: {
|
||||
network_id: 108,
|
||||
networkCheckTimeout: 100000,
|
||||
provider: function() {
|
||||
return ProviderWrapper.wrapProviderAndStartLocalNode(new HDWalletProvider(mnemonic, "http://127.0.0.1:8545/", 0, 10));
|
||||
},
|
||||
gasPrice: gasPrice,
|
||||
gas: gas,
|
||||
},
|
||||
},
|
||||
|
||||
// Set default mocha options here, use special reporters etc.
|
||||
mocha: {
|
||||
timeout: 100000
|
||||
},
|
||||
|
||||
compilers: {
|
||||
solc: {
|
||||
// Add path to the solc-transpiler
|
||||
version: "./node_modules/@eth-optimism/solc",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Use this file to configure your truffle project. It's seeded with some
|
||||
* common settings for different networks and features like migrations,
|
||||
* compilation and testing. Uncomment the ones you need or modify
|
||||
* them to suit your project as necessary.
|
||||
*
|
||||
* More information about configuration can be found at:
|
||||
*
|
||||
* truffleframework.com/docs/advanced/configuration
|
||||
*
|
||||
* To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider)
|
||||
* to sign your transactions before they're sent to a remote public node. Infura accounts
|
||||
* are available for free at: infura.io/register.
|
||||
*
|
||||
* You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
|
||||
* public/private key pairs. If you're publishing your code to GitHub make sure you load this
|
||||
* phrase from a file you've .gitignored so it doesn't accidentally become public.
|
||||
*
|
||||
*/
|
||||
|
||||
// const HDWalletProvider = require('@truffle/hdwallet-provider');
|
||||
// const infuraKey = "fj4jll3k.....";
|
||||
//
|
||||
// const fs = require('fs');
|
||||
// const mnemonic = fs.readFileSync(".secret").toString().trim();
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Networks define how you connect to your ethereum client and let you set the
|
||||
* defaults web3 uses to send transactions. If you don't specify one truffle
|
||||
* will spin up a development blockchain for you on port 9545 when you
|
||||
* run `develop` or `test`. You can ask a truffle command to use a specific
|
||||
* network from the command line, e.g
|
||||
*
|
||||
* $ truffle test --network <network-name>
|
||||
*/
|
||||
|
||||
networks: {
|
||||
// Useful for testing. The `development` name is special - truffle uses it by default
|
||||
// if it's defined here and no other network is specified at the command line.
|
||||
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
|
||||
// tab if you use this network and you must also set the `host`, `port` and `network_id`
|
||||
// options below to some value.
|
||||
//
|
||||
// development: {
|
||||
// host: "127.0.0.1", // Localhost (default: none)
|
||||
// port: 8545, // Standard Ethereum port (default: none)
|
||||
// network_id: "*", // Any network (default: none)
|
||||
// },
|
||||
|
||||
// Another network with more advanced options...
|
||||
// advanced: {
|
||||
// port: 8777, // Custom port
|
||||
// network_id: 1342, // Custom network
|
||||
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
|
||||
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
|
||||
// from: <address>, // Account to send txs from (default: accounts[0])
|
||||
// websockets: true // Enable EventEmitter interface for web3 (default: false)
|
||||
// },
|
||||
|
||||
// Useful for deploying to a public network.
|
||||
// NB: It's important to wrap the provider as a function.
|
||||
// ropsten: {
|
||||
// provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
|
||||
// network_id: 3, // Ropsten's id
|
||||
// gas: 5500000, // Ropsten has a lower block limit than mainnet
|
||||
// confirmations: 2, // # of confs to wait between deployments. (default: 0)
|
||||
// timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
|
||||
// skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
|
||||
// },
|
||||
|
||||
// Useful for private networks
|
||||
// private: {
|
||||
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
|
||||
// network_id: 2111, // This network is yours, in the cloud.
|
||||
// production: true // Treats this network as if it was a public net. (default: false)
|
||||
// }
|
||||
},
|
||||
|
||||
// Set default mocha options here, use special reporters etc.
|
||||
mocha: {
|
||||
// timeout: 100000
|
||||
},
|
||||
|
||||
// Configure your compilers
|
||||
compilers: {
|
||||
solc: {
|
||||
version: "./node_modules/solc",
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue