Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion miniserver/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ POLLING_INTERVAL=1000
DEFAULT_NETWORK='eth-local'
GAS_LIMIT='67219000'
#GAS_PRICE='10000000'
GAS_PRICE='413290302'
GAS_PRICE='529942254'
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use type 2 transactions instead of arbitrary numbers, or manually set base fee in emulating local eth blockchain

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good Point 🙏
An example of how to use type 2 transactions would be useful
Initial google shows these EIPS

  • EIP-1559: A transaction pricing mechanism that includes fixed-per-block network fee that is burned and dynamically expands/contracts block sizes to deal with transient congestion.
  • EIP-2178: TransactionType || TransactionPayload is a valid transaction and TransactionType || ReceiptPayload is a valid transaction receipt where TransactionType identifies the format of the transaction and *Payload is the transaction/receipt contents, which are defined in future EIPs.

Will investigate further.

Copy link
Copy Markdown
Collaborator Author

@johnwhitton johnwhitton Sep 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further Investigation
Gives this code example

require("log-timestamp");
const ethers = require("ethers");

const privateKey = ("ADD_YOUR_PRIVATE_KEY_HERE").toString('hex');
const wallet = new ethers.Wallet(privateKey);

const address = wallet.address;
console.log("Public Address:", address);

const httpsUrl = "ADD_YOUR_HTTP_URL_HERE";
console.log("HTTPS Target", httpsUrl);

const init = async function () {
  const httpsProvider = new ethers.providers.JsonRpcProvider(httpsurl);

  let nonce = await httpsProvider.getTransactionCount(address);
  console.log("Nonce:", nonce);

  let feeData = await httpsProvider.getFeeData();
  console.log("Fee Data:", feeData);

  const tx = {
    type: 2,
    nonce: nonce,
    to: "0x8D97689C9818892B700e27F316cc3E41e17fBeb9", // Address to send to
    maxPriorityFeePerGas: feeData["maxPriorityFeePerGas"], // Recommended maxPriorityFeePerGas
    maxFeePerGas: feeData["maxFeePerGas"], // Recommended maxFeePerGas
    value: ethers.utils.parseEther("0.01"), // .01 ETH
    gasLimit: "21000", // basic transaction costs exactly 21000
    chainId: 42, // Ethereum network id
  };
  console.log("Transaction Data:", tx);

  const signedTx = await wallet.signTransaction(tx);
  console.log("Signed Transaction:", signedTx);

  const txHash = ethers.utils.keccak256(signedTx);
  console.log("Precomputed txHash:", txHash);
  console.log(`https://kovan.etherscan.io/tx/${txHash}`);

  httpsProvider.sendTransaction(signedTx).then(console.log);

};

init();

Also debugging eth_estimateGas implies that we are using type2 transactions "type":"0x2",

    requestBody: '{"method":"eth_estimateGas","params":[{"type":"0x2","maxFeePerGas":"0x77359400","maxPriorityFeePerGas":"0x77359400","from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","to":"0xe7f1725e7734ce288f8367e1bb143e90bb3f0512","data":"0x3d0594d4000000000000000000000000000000000000000000000000016345785d8a0000000000000000000000000000143a933e79931006b3eb89cbc938587546faf15900000000000000000000000058bb8c7d2c90df970fb01a5cd29c4075c41d3ffb"}],"id":49,"jsonrpc":"2.0"}',

Copy link
Copy Markdown
Collaborator Author

@johnwhitton johnwhitton Sep 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I've updated prepareExecute to retrieve the gasPrice
const gasPrice = await provider.getGasPrice()
This introduces an additional rpc call on each transaction
Note this uses getGasPrice there is also estimateGas which may give a more accurate estimate.

@polymorpher let me know your thoughts on this and whether the additional rpc call is to burdensome, and if you'd prefer to use estimateGas

Also assumption is that this works on all evm compatible chains (Including Harmony) however this has only been tested on hardhat and ganache.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's fine, type 2 is not supported on all chains, including ganache

CACHE='cache'
STATS_PATH='../data/stats.json'
OTP_INTERVAL=60000
Expand Down
84 changes: 84 additions & 0 deletions miniserver/abi/MiniProxy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_logic",
"type": "address"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"stateMutability": "payable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "previousAdmin",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "newAdmin",
"type": "address"
}
],
"name": "AdminChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "beacon",
"type": "address"
}
],
"name": "BeaconUpgraded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "implementation",
"type": "address"
}
],
"name": "Upgraded",
"type": "event"
},
{
"stateMutability": "payable",
"type": "fallback"
},
{
"inputs": [],
"name": "implementation",
"outputs": [
{
"internalType": "address",
"name": "impl",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]
2 changes: 1 addition & 1 deletion miniserver/abi/MiniWallet.json
Original file line number Diff line number Diff line change
Expand Up @@ -1043,4 +1043,4 @@
"stateMutability": "nonpayable",
"type": "function"
}
],
]
7 changes: 5 additions & 2 deletions miniserver/blockchain.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const init = async () => {
Logger.log('Initializing blockchain for server')
Logger.log(`network=${config.defaultNetwork}, ${JSON.stringify(networkConfig)}`)
provider = ethers.getDefaultProvider(networkConfig.url)
miniWallet = new ethers.Contract(networkConfig.miniWalletAddress, MiniWallet.abi, provider)
miniWallet = new ethers.Contract(networkConfig.miniWalletAddress, MiniWallet, provider)
provider.pollingInterval = config.pollingInterval
signers.splice(0, signers.length)
if (networkConfig.mnemonic) {
Expand Down Expand Up @@ -72,11 +72,14 @@ const sampleExecutionAddress = () => {

// basic executor used to send funds
const prepareExecute = (logger = Logger.log, abortUnlessRPCError = true) => async (method, ...params) => {
console.log(`method: ${method}`)
console.log(`params: ${JSON.stringify(params)}`)
const fromIndex = sampleExecutionAddress()
const from = signers[fromIndex].address
const miniWalletSigner = miniWallet.connect(signers[fromIndex])
logger(`Sampled [${fromIndex}] ${from}`)
const latestNonce = await rpc.getNonce({ address: from, network: config.defaultNetwork })
const gasPrice = await provider.getGasPrice()
const snapshotPendingNonces = pendingNonces[from]
const nonce = latestNonce + snapshotPendingNonces
pendingNonces[from] += 1
Expand All @@ -89,7 +92,7 @@ const prepareExecute = (logger = Logger.log, abortUnlessRPCError = true) => asyn
const tx = await backOff(
async () => miniWalletSigner[method](...params, {
nonce,
gasPrice: ethers.BigNumber.from(config.gasPrice).mul((numAttempts || 0) + 1),
gasPrice: gasPrice.mul((numAttempts || 0) + 1),
value: 0,
}), {
retry: (ex, n) => {
Expand Down
22 changes: 11 additions & 11 deletions miniserver/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,47 +39,47 @@ const parseSMS = async (req, res, next) => {
res.status(status).send(response.toString())
}
try {
const u = await User.findByPhone({ phone: senderPhoneNumber })
if (!u) {
const { address: fromAddress } = (await User.findByPhone({ phone: senderPhoneNumber })) || {}
if (!fromAddress) {
return respond('You are not registered. Signup at https://smswallet.xyz')
}
if (smsParams.length < 1) {
return respond('error: empty sms command')
}
if (smsParams[0] === 'b') {
req.processedBody = { ...req.processedBody, command: 'balance', fromAddress: u.address }
req.processedBody = { ...req.processedBody, command: 'balance', fromAddress }
return next()
}
if (smsParams[0] === 'p') {
if (smsParams.length < 2) {
return respond('error: pay request requires funder and an amount. example request "p +14158401999 0.1"')
return respond('error: pay request requires recipient and an amount. example request "p +16505473175 0.1"')
}
if (!(toNumber(smsParams[2]) > 0)) {
return respond(`error: pay request requires a valid amount': ${smsParams[2]} example request "p +14158401999 0.1"`)
return respond(`error: pay request requires a valid amount': ${smsParams[2]} example request "p +116505473175 0.1"`)
}
const amount = ethers.utils.parseEther(smsParams[2])
let toAddress
// Allow requesting of funds from users by address (without checking registered phone number)
// Allow sending of funds to users by address (without checking registered phone number)
if (smsParams[1].substr(0, 2) === '0x') {
if (!isValidAddress(smsParams[1])) {
return respond(`error: invalid funder address ${smsParams[1]}. example request "p 0x8ba1f109551bd432803012645ac136ddd64dba72 0.1"`)
return respond(`error: invalid recipient address ${smsParams[1]}. example request "p 0x58bB8c7D2c90dF970fb01a5cD29c4075C41d3FFB 0.1"`)
}
toAddress = checkSumAddress(smsParams[1])
} else {
const { isValid, phoneNumber } = phone(smsParams[1], smsParams[1] === '+' ? undefined : phone(u.phone).countryIso3)
if (!isValid) {
return respond(`error: invalid recipient phone number ${smsParams[1]}. example request "p +14158401999 0.1"`)
return respond(`error: invalid recipient phone number ${smsParams[1]}. example request "p +16505473175 0.1"`)
}
const u2 = await User.findByPhone({ phone: phoneNumber })
if (!u2?.address) {
return respond(`error: funders phone number is not a registered wallet: ${smsParams[1]}. example request "p +14158401999 0.1"`)
return respond(`error: recipients phone number is not a registered wallet: ${smsParams[1]}. example request "p +16505473175 0.1"`)
}
toAddress = u2.address
}
req.processedBody = { ...req.processedBody, command: 'pay', fromAddress: u.address, toAddress, amount }
req.processedBody = { ...req.processedBody, command: 'pay', fromAddress, toAddress, amount }
return next()
}
return respond('error: invalid sms command. example payment request "p +14158401999 0.1"')
return respond('error: invalid sms command. example payment request "p +16505473175 0.1"')
} catch (ex) {
console.error(ex)
return respond('An unexpected occurred. Please contact support.')
Expand Down
17 changes: 12 additions & 5 deletions miniwallet/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ MINIWALLET_INITIAL_OPERATORS=["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","0x70
MINIWALLET_INITIAL_USER_LIMIT=1000000
MINIWALLET_INITIAL_AUTH_LIMIT=100000

# Smart Contract Testing config
# Smart Contract ethlocal config
## MiniWallet
TEST_MINIWALLET_INITIAL_OPERATOR_THRESHOLD=10
TEST_MINIWALLET_INITIAL_OPERATORS=["0x70997970C51812dc3A010C7d01b50e0d17dc79C8","0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC","0x90F79bf6EB2c4f870365E785982E1f101E93b906"]
TEST_MINIWALLET_INITIAL_USER_LIMIT=1000
TEST_MINIWALLET_INITIAL_AUTH_LIMIT=100
ETH_LOCAL_MINIWALLET_INITIAL_OPERATOR_THRESHOLD=10
ETH_LOCAL_MINIWALLET_INITIAL_OPERATORS=["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","0x70997970C51812dc3A010C7d01b50e0d17dc79C8","0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC","0x90F79bf6EB2c4f870365E785982E1f101E93b906"]
ETH_LOCAL_MINIWALLET_INITIAL_USER_LIMIT=1000
ETH_LOCAL_MINIWALLET_INITIAL_AUTH_LIMIT=100

# Smart Contract hardhat config (used for testing e.g. in `yarn test`)
## MiniWallet
HARDHAT_MINIWALLET_INITIAL_OPERATOR_THRESHOLD=10
HARDHAT_MINIWALLET_INITIAL_OPERATORS=["0x70997970C51812dc3A010C7d01b50e0d17dc79C8","0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC","0x90F79bf6EB2c4f870365E785982E1f101E93b906"]
HARDHAT_MINIWALLET_INITIAL_USER_LIMIT=1000
HARDHAT_MINIWALLET_INITIAL_AUTH_LIMIT=100
11 changes: 4 additions & 7 deletions miniwallet/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ import 'dotenv/config'

export default {
test: {
operator: JSON.parse(process.env.TEST_MINIWALLET_INITIAL_OPERATORS || '[]')[0] || '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
user: process.env.TEST_USER || '0xEf4634BdBc6F6528EacB49278d7E17BCB9e2689A',
creator: process.env.TEST_CREATOR || '0x1cf6490889A92371fdBC610C4A862061F28BaFfA',
miniWallet: {
initialOperatorThreshold: process.env.TEST_MINIWALLET_INITIAL_OPERATOR_THRESHOLD,
initialOperators: JSON.parse(process.env.TEST_MINIWALLET_INITIAL_OPERATORS || '[]'),
initialUserLimit: ethers.utils.parseEther(process.env.TEST_MINIWALLET_INIITIAL_USER_LIMIT || '1000'),
initialAuthLimit: ethers.utils.parseEther(process.env.TEST_MINIWALLET_INIITIAL_AUTH_LIMIT || '100')
initialOperatorThreshold: process.env.HARDHAT_MINIWALLET_INITIAL_OPERATOR_THRESHOLD || '10',
initialOperators: JSON.parse(process.env.HARDHAT_MINIWALLET_INITIAL_OPERATORS || '[]'),
initialUserLimit: ethers.utils.parseEther(process.env.HARDHAT_MINIWALLET_INIITIAL_USER_LIMIT || '1000'),
initialAuthLimit: ethers.utils.parseEther(process.env.HARDHAT_MINIWALLET_INIITIAL_AUTH_LIMIT || '100')
}
}
}
18 changes: 17 additions & 1 deletion miniwallet/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,26 @@ import 'dotenv/config'
export default {
mainnet: {
miniWallet: {
initialOperatorThreshold: process.env.MINIWALLET_INITIAL_OPERATOR_THRESHOLD,
initialOperatorThreshold: process.env.MINIWALLET_INITIAL_OPERATOR_THRESHOLD || '100',
initialOperators: JSON.parse(process.env.MINIWALLET_INITIAL_OPERATORS || '[]'),
initialUserLimit: ethers.utils.parseEther(process.env.MINIWALLET_INIITIAL_USER_LIMIT || '1000000'),
initialAuthLimit: ethers.utils.parseEther(process.env.MINIWALLET_INIITIAL_AUTH_LIMIT || '100000')
}
},
ethlocal: {
miniWallet: {
initialOperatorThreshold: process.env.ETH_LOCAL_MINIWALLET_INITIAL_OPERATOR_THRESHOLD || '10',
initialOperators: JSON.parse(process.env.ETH_LOCAL_MINIWALLET_INITIAL_OPERATORS || '[]'),
initialUserLimit: ethers.utils.parseEther(process.env.ETH_LOCAL_MINIWALLET_INIITIAL_USER_LIMIT || '1000'),
initialAuthLimit: ethers.utils.parseEther(process.env.ETH_LOCAL_MINIWALLET_INIITIAL_AUTH_LIMIT || '100')
}
},
hardhat: {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference between ethLocal and hardhat config?

Copy link
Copy Markdown
Collaborator Author

@johnwhitton johnwhitton Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ethLocal is used for end to end testing. It is part or the local testing environment documented here specifically using ganache as documented here

hardhat config is used by the following commands

Note the tags are populated in each of the test scripts
e.g. in 001_deploy_miniWallet.ts we use the following tags deployFunction.tags = ['MiniWallet', 'deploy', 'MiniWalletHardhatDeploy']
There are additional control mechanisms which can be leveraged for sequencing if needed.

miniWallet: {
initialOperatorThreshold: process.env.HARDHAT_MINIWALLET_INITIAL_OPERATOR_THRESHOLD || '10',
initialOperators: JSON.parse(process.env.HARDHAT_MINIWALLET_INITIAL_OPERATORS || '[]'),
initialUserLimit: ethers.utils.parseEther(process.env.HARDHAT_MINIWALLET_INIITIAL_USER_LIMIT || '1000'),
initialAuthLimit: ethers.utils.parseEther(process.env.HARDHAT_MINIWALLET_INIITIAL_AUTH_LIMIT || '100')
}
}
}
57 changes: 57 additions & 0 deletions miniwallet/contracts/mocks/TestERC1155.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

/// @custom:security-contact dev@modulo.so
contract TestERC1155 is ERC1155, AccessControl {
bytes32 public constant URI_SETTER_ROLE = keccak256("URI_SETTER_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

constructor(uint256[] memory tokenIds, uint256[] memory amounts)
ERC1155("")
{
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(URI_SETTER_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
for (uint32 i = 0; i < tokenIds.length; i++) {
mint(msg.sender, tokenIds[i], amounts[i], "");
}
}

function setURI(string memory newuri) public onlyRole(URI_SETTER_ROLE) {
_setURI(newuri);
}

function mint(
address account,
uint256 id,
uint256 amount,
bytes memory data
) public onlyRole(MINTER_ROLE) {
_mint(account, id, amount, data);
}

function mintBatch(
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public onlyRole(MINTER_ROLE) {
_mintBatch(to, ids, amounts, data);
}

// The following functions are overrides required by Solidity.

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC1155, AccessControl)
returns (bool)
{
return (ERC1155.supportsInterface(interfaceId) ||
AccessControl.supportsInterface(interfaceId));
}
}
21 changes: 21 additions & 0 deletions miniwallet/contracts/mocks/TestERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

/// @custom:security-contact dev@modulo.so
contract TestERC20 is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

constructor(uint256 _amount) ERC20("TestERC20", "T20") {
_mint(msg.sender, _amount * 10**decimals());
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}

function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}
45 changes: 45 additions & 0 deletions miniwallet/contracts/mocks/TestERC721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

/// @custom:security-contact dev@modulo.so
contract TestERC721 is ERC721, AccessControl {
using Counters for Counters.Counter;

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
Counters.Counter private _tokenIdCounter;

constructor(uint256 _amount) ERC721("TestERC721", "T721") {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
for (uint32 i = 0; i < _amount; i++) {
safeMint(msg.sender);
}
}

function _baseURI() internal pure override returns (string memory) {
return "";
}

function safeMint(address to) public onlyRole(MINTER_ROLE) {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
}

// The following functions are overrides required by Solidity.

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, AccessControl)
returns (bool)
{
return (ERC721.supportsInterface(interfaceId) ||
AccessControl.supportsInterface(interfaceId));
}
}
Loading