-
Notifications
You must be signed in to change notification settings - Fork 3
Miniwallet enhancements (including miniserver and smart contract changes) #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c0473e2
ac251c4
e45388c
a234cf9
70ee740
4b988b4
a47c25b
86c7933
9d4ab08
b55e00c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| module.exports = { | ||
| root: true, | ||
| env: { | ||
| node: true, | ||
| es2020: true, | ||
| jest: true, | ||
| }, | ||
| extends: [ | ||
| 'standard', | ||
| ], | ||
| globals: { | ||
| artifacts: 'readonly', | ||
| contract: 'readonly', | ||
| assert: 'readonly', | ||
| ethers: 'readonly' | ||
| }, | ||
| // 'parser': '@babel/eslint-parser', | ||
| rules: { | ||
| 'no-await-in-loop': 0, | ||
| 'no-underscore-dangle': 0, | ||
| 'import/prefer-default-export': 0, | ||
| 'import/no-extraneous-dependencies': 1, | ||
| 'comma-dangle': 0, | ||
| 'no-console': 0, | ||
| 'no-mixed-operators': 0, | ||
| 'new-cap': 0, | ||
| 'max-len': 0, | ||
| }, | ||
| parserOptions: { | ||
| requireConfigFile: false, | ||
| ecmaVersion: 2020 | ||
| }, | ||
| plugins: [ | ||
| '@babel', | ||
| ], | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| /credentials | ||
| /certs/* | ||
| !/certs/README.md | ||
| !/certs/gen.sh |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| require('dotenv').config() | ||
| const createError = require('http-errors') | ||
| // const rateLimit = require('express-rate-limit') | ||
| const Fingerprint = require('express-fingerprint') | ||
| const express = require('express') | ||
| const path = require('path') | ||
| const cookieParser = require('cookie-parser') | ||
| const logger = require('morgan') | ||
| const config = require('./config') | ||
| const _index = require('./routes/index') | ||
| const bodyParser = require('body-parser') | ||
| const app = express() | ||
| const https = require('https') | ||
| const http = require('http') | ||
| const env = process.env.NODE_ENV || 'development' | ||
| const fs = require('fs') | ||
| const blockchain = require('./blockchain') | ||
|
|
||
| Error.stackTraceLimit = 100 | ||
| app.locals.ENV = env | ||
| app.locals.ENV_DEVELOPMENT = env === 'development' | ||
|
|
||
| app.set('trust proxy', true) | ||
|
|
||
| try { | ||
| blockchain.init().catch(ex => { | ||
| console.error('Blockchain initialization failed') | ||
| console.error(ex) | ||
| process.exit(2) | ||
| }) | ||
| } catch (ex) { | ||
| console.error(ex) | ||
| process.exit(1) | ||
| } | ||
|
|
||
| let httpServer, httpsServer | ||
|
|
||
| const httpsOptions = { | ||
| key: fs.readFileSync(config.https.key), | ||
| cert: fs.readFileSync(config.https.cert) | ||
| } | ||
|
|
||
| if (config.https.only) { | ||
| const httpApp = express() | ||
| const httpRouter = express.Router() | ||
| httpApp.use('*', httpRouter) | ||
| httpRouter.get('*', function (req, res) { | ||
| const hostPort = (req.get('host') || '').split(':') | ||
| const url = hostPort.length === 2 ? `https://${hostPort[0]}:${config.httpsPort}${req.originalUrl}` : `https://${hostPort[0]}${req.originalUrl}` | ||
| res.redirect(url) | ||
| }) | ||
| httpServer = http.createServer(httpApp) | ||
| } else { | ||
| httpServer = http.createServer(app) | ||
| } | ||
|
|
||
| httpsServer = https.createServer(httpsOptions, app) | ||
|
|
||
| app.use(Fingerprint({ | ||
| parameters: [ | ||
| Fingerprint.useragent, | ||
| Fingerprint.acceptHeaders, | ||
| Fingerprint.geoip, | ||
| ] | ||
| })) | ||
|
|
||
| app.use(bodyParser.json({ | ||
| verify: function (req, _res, buf) { | ||
| req.rawBody = buf | ||
| } | ||
| })) | ||
| app.use(bodyParser.urlencoded({ extended: true })) | ||
| app.use(logger('dev')) | ||
| app.use(express.json()) | ||
| app.use(express.urlencoded({ extended: false })) | ||
| app.use(cookieParser()) | ||
|
|
||
| if (config.corsOrigins) { | ||
| app.use((req, res, next) => { | ||
| // res.header('Access-Control-Allow-Origin', config.corsOrigins) | ||
| if (config.corsOrigins === '*' || config.corsOrigins.indexOf(req.headers.origin) >= 0) { | ||
| res.header('Access-Control-Allow-Origin', req.headers.origin || config.corsOrigins) | ||
| } else { | ||
| res.header('Access-Control-Allow-Origin', config.corsOrigins) | ||
| } | ||
|
|
||
| res.header('Access-Control-Allow-Credentials', 'true') | ||
|
|
||
| res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS') | ||
|
|
||
| res.header('Access-Control-Allow-Headers', 'X-SECRET, X-NETWORK, X-MAJOR-VERSION, X-MINOR-VERSION, Accept, Accept-CH, Accept-Charset, Accept-Datetime, Accept-Encoding, Accept-Ext, Accept-Features, Accept-Language, Accept-Params, Accept-Ranges, Access-Control-Allow-Credentials, Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin, Access-Control-Expose-Headers, Access-Control-Max-Age, Access-Control-Request-Headers, Access-Control-Request-Method, Age, Allow, Alternates, Authentication-Info, Authorization, C-Ext, C-Man, C-Opt, C-PEP, C-PEP-Info, CONNECT, Cache-Control, Compliance, Connection, Content-Base, Content-Disposition, Content-Encoding, Content-ID, Content-Language, Content-Length, Content-Location, Content-MD5, Content-Range, Content-Script-Type, Content-Security-Policy, Content-Style-Type, Content-Transfer-Encoding, Content-Type, Content-Version, Cookie, Cost, DAV, DELETE, DNT, DPR, Date, Default-Style, Delta-Base, Depth, Derived-From, Destination, Differential-ID, Digest, ETag, Expect, Expires, Ext, From, GET, GetProfile, HEAD, HTTP-date, Host, IM, If, If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Keep-Alive, Label, Last-Event-ID, Last-Modified, Link, Location, Lock-Token, MIME-Version, Man, Max-Forwards, Media-Range, Message-ID, Meter, Negotiate, Non-Compliance, OPTION, OPTIONS, OWS, Opt, Optional, Ordering-Type, Origin, Overwrite, P3P, PEP, PICS-Label, POST, PUT, Pep-Info, Permanent, Position, Pragma, ProfileObject, Protocol, Protocol-Query, Protocol-Request, Proxy-Authenticate, Proxy-Authentication-Info, Proxy-Authorization, Proxy-Features, Proxy-Instruction, Public, RWS, Range, Referer, Refresh, Resolution-Hint, Resolver-Location, Retry-After, Safe, Sec-Websocket-Extensions, Sec-Websocket-Key, Sec-Websocket-Origin, Sec-Websocket-Protocol, Sec-Websocket-Version, Security-Scheme, Server, Set-Cookie, Set-Cookie2, SetProfile, SoapAction, Status, Status-URI, Strict-Transport-Security, SubOK, Subst, Surrogate-Capability, Surrogate-Control, TCN, TE, TRACE, Timeout, Title, Trailer, Transfer-Encoding, UA-Color, UA-Media, UA-Pixels, UA-Resolution, UA-Windowpixels, URI, Upgrade, User-Agent, Variant-Vary, Vary, Version, Via, Viewport-Width, WWW-Authenticate, Want-Digest, Warning, Width, X-Content-Duration, X-Content-Security-Policy, X-Content-Type-Options, X-CustomHeader, X-DNSPrefetch-Control, X-Forwarded-For, X-Forwarded-Port, X-Forwarded-Proto, X-Frame-Options, X-Modified, X-OTHER, X-PING, X-PINGOTHER, X-Powered-By, X-Requested-With') | ||
| next() | ||
| }) | ||
| } | ||
|
|
||
| app.use(express.static(path.join(__dirname, 'public'))) | ||
| app.options('*', async (_req, res) => res.end()) | ||
| app.use('/', _index) | ||
|
|
||
| // catch 404 and forward to error handler | ||
| app.use(function (req, res, next) { | ||
| next(createError(404)) | ||
| }) | ||
|
|
||
| // error handler | ||
| app.use(function (err, req, res, next) { | ||
| // set locals, only providing error in development | ||
| res.locals.message = err.message | ||
| res.locals.error = config.debug ? err : {} | ||
|
|
||
| // render the error page | ||
| res.status(err.status || 500) | ||
| res.json({ error: res.locals.error, message: err.message }) | ||
| }) | ||
|
|
||
| module.exports = { | ||
| httpServer, | ||
| httpsServer | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| #!/usr/bin/env node | ||
| const apps = require('../app') | ||
| const config = require('../config') | ||
| const httpsServer = apps.httpsServer | ||
| const httpServer = apps.httpServer | ||
| console.log('Starting web server...') | ||
|
|
||
| httpsServer.listen(config.httpsPort || 8443, () => { | ||
| const addr = httpsServer.address() | ||
| console.log(`HTTPS server listening on port ${addr.port} at ${addr.address}`) | ||
| }) | ||
|
|
||
| httpServer.listen(config.port || 3000, () => { | ||
| const addr = httpServer.address() | ||
| console.log(`HTTP server listening on port ${addr.port} at ${addr.address}`) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| const config = require('./config') | ||
| const { ethers } = require('ethers') | ||
| const { Logger } = require('./logger') | ||
| const cloneDeep = require('lodash/fp/cloneDeep') | ||
| const { backOff } = require('exponential-backoff') | ||
| const { rpc } = require('./rpc') | ||
| const MiniWallet = require('../miniwallet/build/contracts/MiniWallet.sol/MiniWallet.json') | ||
|
|
||
| let networkConfig = {} | ||
| let provider | ||
| const pendingNonces = {} | ||
| const signers = [] | ||
| const miniWallets = [] | ||
| const walletPath = 'm/44\'/60\'/0\'/0/' // https://docs.ethers.io/v5/api/signer/#Wallet.fromMnemonic' | ||
|
|
||
| const init = async () => { | ||
| Logger.log('Initializing blockchain for server') | ||
| try { | ||
| Logger.log(`config.defaultNetwork: ${config.defaultNetwork}`) | ||
| networkConfig = config.networks[config.defaultNetwork] | ||
| Logger.log(`network: ${JSON.stringify(networkConfig)}`) | ||
| provider = ethers.getDefaultProvider(networkConfig.url) | ||
| provider.pollingInterval = config.pollingInterval | ||
| if (networkConfig.mnemonic) { | ||
| for (let i = 0; i < networkConfig.numAccounts; i += 1) { | ||
| const path = walletPath + i.toString() | ||
| Logger.log(`path: ${path}`) | ||
| const signer = new ethers.Wallet.fromMnemonic(networkConfig.mnemonic, path) | ||
| signers[i] = signer.connect(provider) | ||
| } | ||
| } else { | ||
| signers[0] = new ethers.Wallet(networkConfig.key, networkConfig.provider) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. signers has length 0 by default
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what the ask here is |
||
| } | ||
| for (let i = 0; i < signers.length; i += 1) { | ||
| Logger.log(`signers[${i}].address; ${JSON.stringify(signers[i].address)}`) | ||
| miniWallets[i] = new ethers.Contract(networkConfig.miniWalletAddress, MiniWallet.abi, signers[i]) | ||
johnwhitton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } catch (ex) { | ||
| console.error(ex) | ||
| console.trace(ex) | ||
| } | ||
| for (const signer of signers) { | ||
| pendingNonces[signer.address] = 0 | ||
| Logger.log(`[${config.defaultNetwork}][${signer.address}] Set pending nonce = 0`) | ||
| } | ||
| } | ||
|
|
||
| const sampleExecutionAddress = () => { | ||
| const nonces = cloneDeep(pendingNonces) | ||
| const probs = [] | ||
| let sum = 0 | ||
| for (const signer of signers) { | ||
| const p = 1.0 / Math.exp(nonces[signer.address]) | ||
| probs.push(p) | ||
| sum += p | ||
| } | ||
| const r = Math.random() * sum | ||
| let s = 0 | ||
| for (let i = 0; i < probs.length; i++) { | ||
| s += probs[i] | ||
| if (s >= r) { | ||
| return [i, signers[i].address, miniWallets[i]] | ||
| } | ||
| } | ||
| return [signers.length - 1, signers[signers.length - 1].address, miniWallets[miniWallets.length - 1]] | ||
johnwhitton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // basic executor used to send funds | ||
| const prepareExecute = (logger = Logger.log, abortUnlessRPCError = true) => async (method, params) => { | ||
| const [fromIndex, from, miniWallet] = sampleExecutionAddress() | ||
| logger(`Sampled [${fromIndex}] ${from}`) | ||
| const latestNonce = await rpc.getNonce({ address: from, network: config.defaultNetwork }) | ||
| const snapshotPendingNonces = pendingNonces[from] | ||
| const nonce = latestNonce + snapshotPendingNonces | ||
| pendingNonces[from] += 1 | ||
| const t0 = performance.now() | ||
| const elapsed = () => (performance.now() - t0).toFixed(3) | ||
| const printNonceStats = () => `[elapsed=${elapsed()}ms][network=${config.defaultNetwork}][account=${fromIndex}][nonce=${nonce}][snapshot=${snapshotPendingNonces}][current=${pendingNonces[from]}]` | ||
| try { | ||
| logger(`[pending]${printNonceStats()}`) | ||
| let numAttempts = 0 | ||
| // const f = () => {miniWallet.send(params[0], params[1], params[2])} | ||
| const tx = await backOff( | ||
| async () => miniWallet.send(params[0], params[1], params[2], { | ||
johnwhitton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| nonce, | ||
| // nonce: '0x' + ethers.BigNumber.from(nonce).toHexString, | ||
| gasPrice: ethers.BigNumber.from(config.gasPrice).mul((numAttempts || 0) + 1), | ||
| value: 0, | ||
| }), { | ||
| retry: (ex, n) => { | ||
| if (ex?.abort) { | ||
| console.error('[error-abort]', ex) | ||
| logger(`[abort][attempts=${n}]${printNonceStats()}`) | ||
| return false | ||
| } | ||
| if (!ex?.receipt && !ex?.response?.data && abortUnlessRPCError) { | ||
| console.error('[error-abort-before-rpc]', ex) | ||
| logger(`[abort-before-rpc][attempts=${n}]${printNonceStats()}`) | ||
| return false | ||
| } | ||
| console.error('[error]', ex?.response?.status, ex) | ||
| numAttempts = n | ||
| logger(`[retry][attempts=${n}]${printNonceStats()}`) | ||
| return true | ||
| } | ||
| }) | ||
| logger(`[complete]${printNonceStats()}`, JSON.stringify(tx, null, 2)) | ||
| return tx | ||
| } catch (ex) { | ||
| logger(`[error]${printNonceStats()}`, ex) | ||
| throw ex | ||
| } finally { | ||
| pendingNonces[from] -= 1 | ||
| } | ||
| } | ||
|
|
||
| module.exports = { | ||
| init, | ||
| getNetworkConfig: () => networkConfig, | ||
| getProvider: () => provider, | ||
| getSigners: () => signers, | ||
| getMiniWallets: () => miniWallets, | ||
| prepareExecute | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| For hosting temporary credentials. Run `./gen.sh` to generate a local dev certificate |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| !/bin/bash | ||
| openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 \ | ||
| -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=sms-wallet.local" \ | ||
| -keyout test.key -out test.cert |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| require('dotenv').config() | ||
johnwhitton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const ethers = require('ethers') | ||
| const DEBUG = process.env.MINISERVER_DEBUG === 'true' || process.env.MINISERVER_DEBUG === '1' | ||
| const config = { | ||
| debug: DEBUG, | ||
| url: process.env.SERVER_URL || 'https://localhost', | ||
| port: process.env.PORT || 3000, | ||
| httpsPort: process.env.HTTPS_PORT || 8443, | ||
| nullAddress: '0x0000000000000000000000000000000000000000', | ||
| verbose: process.env.VERBOSE === 'true' || process.env.VERBOSE === '1', | ||
| https: { | ||
| only: process.env.HTTPS_ONLY === 'true' || process.env.HTTPS_ONLY === '1', | ||
| key: DEBUG ? './certs/test.key' : './certs/privkey.pem', | ||
| cert: DEBUG ? './certs/test.cert' : './certs/fullchain.pem' | ||
| }, | ||
| corsOrigins: process.env.CORS, | ||
| secret: process.env.SECRET, | ||
| safeNonce: process.env.SAFE_NONCE === '1' || process.env.SAFE_NONCE === 'true', | ||
| pollingInterval: parseInt(process.env.POLLING_INTERVAL || 1000), | ||
| defaultNetwork: process.env.DEFAULT_NETWORK || 'eth-local', | ||
| networks: { | ||
| 'harmony-testnet': { | ||
| key: process.env.HARMONY_TESTNET_KEY || '', | ||
| url: process.env.TESTNET_RPC || 'https://api.s0.b.hmny.io', | ||
| wss: process.env.TESTNET_WSS, | ||
| mnemonic: process.env.HARMONY_TESTNET_MNEMONIC, | ||
| skip: process.env.SKIP_TESTNE || true, | ||
| numAccounts: process.env.TESTNET_NUM_ACCOUNTS || 1, | ||
| blockTime: 2, | ||
| miniWalletAddress: process.env.TESTNET_ASSET_MANAGER, | ||
| }, | ||
| 'harmony-mainnet': { | ||
| key: process.env.HARMONY_MAINNET_KEY || '', | ||
| beacon: process.env.BEACON_MAINNET_RPC, | ||
| url: process.env.MAINNET_RPC || process.env.BEACON_MAINNET_RPC || 'https://api.s0.t.hmny.io', | ||
| wss: process.env.MAINNET_WSS || process.env.BEACON_MAINNET_WSS, | ||
| mnemonic: process.env.HARMONY_MAINNET_MNEMONIC, | ||
| skip: process.env.SKIP_MAINNET || true, | ||
| numAccounts: process.env.MAINNET_NUM_ACCOUNTS || 1, | ||
| blockTime: 2, | ||
| miniWalletAddress: process.env.MAINNET_ASSET_MANAGER, | ||
| }, | ||
| 'eth-local': { | ||
| url: process.env.ETH_LOCAL_RPC || 'http://127.0.0.1:8545', | ||
| wss: process.env.ETH_LOCAL_WSS, | ||
| key: process.env.ETH_LOCAL_KEY, | ||
| mnemonic: process.env.ETH_LOCAL_MNEMONIC, | ||
| skip: process.env.SKIP_ETH || true, | ||
| numAccounts: process.env.ETH_LOCAL_NUM_ACCOUNTS || 1, | ||
| miniWalletAddress: process.env.ETH_LOCAL_ASSET_MANAGER, | ||
| }, | ||
| }, | ||
| gasLimit: parseInt(process.env.GAS_LIMIT || '12345678'), | ||
| gasPrice: ethers.BigNumber.from(process.env.GAS_PRICE || '200'), | ||
| stats: { | ||
| // relevant to relayer root directory | ||
| path: process.env.STATS_PATH || '../data/stats.json' | ||
| }, | ||
|
|
||
| datastore: { | ||
| gceProjectId: process.env.GCP_PROJECT, | ||
| cred: !process.env.GCP_CRED_PATH ? {} : require(process.env.GCP_CRED_PATH), | ||
| mock: !process.env.GCP_CRED_PATH, | ||
| mockPort: 9000, | ||
| namespace: process.env.GCP_NAMESPACE || 'sms-wallet-server' | ||
| }, | ||
|
|
||
| twilio: { | ||
| sid: process.env.TWILIO_ACCOUNT_SID, | ||
| token: process.env.TWILIO_AUTH_TOKEN, | ||
| from: process.env.TWILIO_FROM, | ||
| }, | ||
|
|
||
| otp: { | ||
| salt: process.env.OTP_SALT, | ||
| interval: parseInt(process.env.OTP_INTERVAL || 60000) | ||
| }, | ||
| defaultSignatureValidDuration: 1000 * 60 * 15, | ||
| clientRoot: process.env.CLIENT_ROOT || 'https://smswallet.xyz', | ||
| } | ||
| module.exports = config | ||
Uh oh!
There was an error while loading. Please reload this page.