Skip to content
Closed
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
36 changes: 36 additions & 0 deletions miniserver/.eslintrc.js
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',
],
}
4 changes: 4 additions & 0 deletions miniserver/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/credentials
/certs/*
!/certs/README.md
!/certs/gen.sh
119 changes: 119 additions & 0 deletions miniserver/app.js
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
}
16 changes: 16 additions & 0 deletions miniserver/bin/run.js
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}`)
})
124 changes: 124 additions & 0 deletions miniserver/blockchain.js
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)
Copy link
Owner

Choose a reason for hiding this comment

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

signers has length 0 by default

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure what the ask here is
My thought is we need to set signers[0] when using a privateKey.
@polymorpher can you clarify 🙏

}
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])
}
} 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]]
}

// 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], {
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
}
1 change: 1 addition & 0 deletions miniserver/certs/README.md
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
4 changes: 4 additions & 0 deletions miniserver/certs/gen.sh
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
81 changes: 81 additions & 0 deletions miniserver/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require('dotenv').config()
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
Loading