Skip to content
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

Implement helper functions #42

Merged
merged 26 commits into from
Oct 23, 2018
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8116071
Scanner, parser, evaluator: Implement helper functions
izqui Sep 20, 2018
8ff3081
Add example helpers
izqui Sep 20, 2018
0d7c770
Make helpers a class and provide eth object to helpers
izqui Sep 21, 2018
b920e9f
Implement tokenAmount helper
izqui Sep 21, 2018
2204535
Lint
izqui Sep 21, 2018
274449c
Fix only testing for helper cases
izqui Sep 21, 2018
8e22222
Implement formatDate helper
izqui Sep 21, 2018
4028cdf
Implement transformTime helper
izqui Sep 21, 2018
dc0de7f
Move formatBN logic to its own file
izqui Sep 24, 2018
f011f3d
Implement formatPct helper
izqui Sep 24, 2018
625fa3e
Lint
izqui Sep 24, 2018
b140217
Document helper functions
izqui Sep 24, 2018
f670e04
Style fix
izqui Sep 24, 2018
03c8478
Strip down ERC20 ABI to only have the functions that we use
izqui Sep 27, 2018
c6cd403
Migrate from moment.js to date-fns
izqui Sep 27, 2018
973616d
Add explicit number base
izqui Oct 8, 2018
4352ea7
Improve formatBN
izqui Oct 8, 2018
fde131c
Merge branch 'master' into helpers
sohkai Oct 21, 2018
8c92fa8
Fix linting
sohkai Oct 21, 2018
8a2e04c
Update and pin date-fns to 2.0.0-beta.22
sohkai Oct 21, 2018
06c6811
Refactor number assumptions and conversion in helper functions
sohkai Oct 21, 2018
6bc383f
Refactor transformTime helper to not use dynamic requires
sohkai Oct 21, 2018
b390a29
Export default helpers directly in helpers module
sohkai Oct 21, 2018
2b02323
Code style
sohkai Oct 21, 2018
fa9943c
Make best toUnit an explicit parameter
izqui Oct 23, 2018
035d1a2
test: add example for using 'best' in transformTime
sohkai Oct 23, 2018
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
4,910 changes: 2,457 additions & 2,453 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"dependencies": {
"bn.js": "4.11.6",
"date-fns": "2.0.0-alpha.7",
"web3-eth": "1.0.0-beta.33",
"web3-eth-abi": "1.0.0-beta.33",
"web3-utils": "1.0.0-beta.33"
Expand Down
17 changes: 16 additions & 1 deletion src/evaluator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const Eth = require('web3-eth')
const Web3Utils = require('web3-utils')
const BN = require('bn.js')
const types = require('../types')
const { Helpers } = require('../helpers')

/**
* A value coupled with a type
Expand Down Expand Up @@ -63,6 +64,7 @@ class Evaluator {
this.bindings = bindings
this.eth = new Eth(ethNode || 'https://mainnet.infura.io')
this.to = to && new TypedValue('address', to)
this.helpers = new Helpers(this.eth)
}

/**
Expand All @@ -81,7 +83,7 @@ class Evaluator {
* Evaluate a single node.
*
* @param {radspec/parser/Node} node
* @return {string}
* @return {Promise<string>}
*/
async evaluateNode (node) {
if (node.type === 'ExpressionStatement') {
Expand Down Expand Up @@ -238,6 +240,19 @@ class Evaluator {
(data) => new TypedValue(returnType, ABI.decodeParameter(returnType, data))
)
}

if (node.type === 'HelperFunction') {
const helperName = node.name

if (!this.helpers.exists(helperName)) {
this.panic(`${helperName} helper function is not defined`)
}

const inputs = await this.evaluateNodes(node.inputs)
const result = await this.helpers.execute(helperName, inputs)

return new TypedValue(result.type, result.value)
}
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/helpers/echo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = () =>
/**
* Repeats a string (testing helper)
*
* @param {string} echo The string
* @param {integer} repeat Number of times to repeat the string
* @return {Promise<radspec/evaluator/TypedValue>}
*/
async (echo, repeat = 1) => {
return {
type: 'string',
value: echo.repeat(repeat)
}
}
16 changes: 16 additions & 0 deletions src/helpers/formatDate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const formatDate = require('date-fns/format')

module.exports = () =>
/**
* Format a timestamp as a string (using date-fns)
*
* @param {integer} timestamp Unix timestamp
* @param {string} format The format for the date (https://date-fns.org/v2.0.0-alpha.7/docs/format)
* @return {Promise<radspec/evaluator/TypedValue>}
*/
async (timestamp, format = 'MM-DD-YYYY') => {
return {
type: 'string',
value: formatDate(new Date(timestamp.toNumber() * 1000), format)
}
}
20 changes: 20 additions & 0 deletions src/helpers/formatPct.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const { formatBN, tenPow } = require('./lib/formatBN')

module.exports = () =>
/**
* Format a percentage amount
*
* @param {integer} value The absolute number that will be formatted as a percentage
* @param {integer} base The number that is considered a 100% when calculating the percentage
* @param {integer} precision The number of decimals that will be printed (if any)
* @return {Promise<radspec/evaluator/TypedValue>}
*/
async (value, base = tenPow(18), precision = 2) => {
const oneHundred = tenPow(2)
const formattedAmount = formatBN(value.mul(oneHundred), base, precision)

return {
type: 'string',
value: `${formattedAmount}`
}
}
55 changes: 55 additions & 0 deletions src/helpers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const formatDate = require('./formatDate')
const echo = require('./echo')
const tokenAmount = require('./tokenAmount')
const transformTime = require('./transformTime')
const formatPct = require('./formatPct')

const defaultHelpers = {
formatDate,
transformTime,
tokenAmount,
formatPct,
echo
}

/**
* Class for managing the execution of helper functions
*
* @class Helpers
* @param {web3/eth} eth web3.eth instance
* @param {Object.<string,helpers/Helper>} userHelpers User defined helpers
*/
class Helpers {
constructor (eth, userHelpers = {}) {
this.eth = eth
this.helpers = { ...defaultHelpers, ...userHelpers }
}

/**
* Does a helper exist
*
* @param {string} helper Helper name
* @return {bool}
*/
exists (helper) {
return !!this.helpers[helper]
}

/**
* Execute a helper with some inputs
*
* @param {string} helper Helper name
* @param {Array<radspec/evaluator/TypedValue>} inputs
* @return {Promise<radspec/evaluator/TypedValue>}
*/
execute (helper, inputs) {
inputs = inputs.map(input => input.value) // pass values directly
return this.helpers[helper](this.eth)(...inputs)
}
}

module.exports = {
Helpers,

defaultHelpers
}
17 changes: 17 additions & 0 deletions src/helpers/lib/formatBN.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const BN = require('bn.js')

exports.tenPow = x => (
(new BN(10)).pow(new BN(x))
)

exports.formatBN = (value, base, precision) => {
// Inspired by: https://github.com/ethjs/ethjs-unit/blob/35d870eae1c32c652da88837a71e252a63a83ebb/src/index.js#L83
const baseLength = base.toString().length

let fraction = value.mod(base).toString()
const zeros = '0'.repeat(Math.max(0, baseLength - fraction.length - 1))
fraction = `${zeros}${fraction}`
const whole = value.div(base).toString()

return `${whole}${parseInt(fraction) === 0 ? '' : `.${fraction.slice(0, precision)}`}`
Copy link

Choose a reason for hiding this comment

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

It shouldn’t be a problem in modern JS engines, but it can still be a good idea to specify the radix as a second parameter of parseInt() for a while, just in case scripts are run on unsupported engines (e.g. old mobile browser or Node.js version) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt#Octal_interpretations_with_no_radix

}
35 changes: 35 additions & 0 deletions src/helpers/lib/token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
exports.ETH = '0x0000000000000000000000000000000000000000'
exports.ERC20_SYMBOL_DECIMALS_ABI = [
{
"constant":true,
"inputs":[

],
"name":"decimals",
"outputs":[
{
"name":"",
"type":"uint8"
}
],
"payable":false,
"stateMutability":"view",
"type":"function"
},
{
"constant":true,
"inputs":[

],
"name":"symbol",
"outputs":[
{
"name":"",
"type":"string"
}
],
"payable":false,
"stateMutability":"view",
"type":"function"
},
]
39 changes: 39 additions & 0 deletions src/helpers/tokenAmount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const BN = require('bn.js')
const { ERC20_SYMBOL_DECIMALS_ABI, ETH } = require('./lib/token')
const { formatBN, tenPow } = require('./lib/formatBN')

module.exports = (eth) =>
/**
* Format token amounts taking decimals into account
*
* @param {string} tokenAddress The address of the token
* @param {integer} amount The absolute amount for the token quantity (wei)
* @param {bool} showSymbol Whether the token symbol will be printed after the amount
* @param {integer} precision The number of decimals that will be printed (if any)
* @return {Promise<radspec/evaluator/TypedValue>}
*/
async (tokenAddress, amount, showSymbol = true, precision = new BN(2)) => {
let decimals
let symbol

if (tokenAddress === ETH) {
decimals = new BN(18)
if (showSymbol) {
symbol = 'ETH'
}
} else {
const token = new eth.Contract(ERC20_SYMBOL_DECIMALS_ABI, tokenAddress)

decimals = new BN(await token.methods.decimals().call())
if (showSymbol) {
symbol = await token.methods.symbol().call()
}
}

const formattedAmount = formatBN(amount, tenPow(decimals), precision)

return {
type: 'string',
value: showSymbol ? `${formattedAmount} ${symbol}` : formattedAmount
}
}
48 changes: 48 additions & 0 deletions src/helpers/transformTime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const formatDistanceStrict = require('date-fns/formatDistanceStrict')

const BEST_UNIT = 'best'
const TO_UNIT_MAP = {
seconds: 's',
minutes: 'm',
hours: 'h',
days: 'd',
months: 'M',
years: 'Y'
}
const SUPPORTED_TO_UNITS = new Set(Object.keys(TO_UNIT_MAP).concat(BEST_UNIT))
const SUPPORTED_FROM_UNITS = new Set([
...Object.keys(TO_UNIT_MAP),
'milliseconds',
'weeks'
])

module.exports = () =>
/**
* Transform between time units.
*
* @param {integer} time The base time amount
* @param {string} toUnit The unit to convert the time to (Supported units: 'seconds', 'minutes', 'hours', 'days', 'months', 'years')
* @param {string} fromUnit The unit to convert the time from (Supported units: 'milliseconds', 'seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years')
* @return {Promise<radspec/evaluator/TypedValue>}
*/
async (time, toUnit = BEST_UNIT, fromUnit = 'seconds') => {
if (!SUPPORTED_FROM_UNITS.has(fromUnit)) {
throw new Error(`@transformTime: Time unit ${fromUnit} is not supported as a fromUnit`)
}

if (!SUPPORTED_TO_UNITS.has(toUnit)) {
throw new Error(`@transformTime: Time unit ${toUnit} is not supported as a toUnit`)
}

const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1)
const add = require(`date-fns/add${capitalize(fromUnit)}`)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unsure if this dynamic require could cause us trouble

Copy link
Contributor

Choose a reason for hiding this comment

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

Pretty sure bundlers aren't going to like this :(. Best to avoid it and add it to a map or similar.


const zeroDate = new Date(0)
const duration = add(zeroDate, time.toNumber())

const options = toUnit === BEST_UNIT ? {} : { unit: TO_UNIT_MAP[toUnit] }
return {
type: 'string',
value: formatDistanceStrict(zeroDate, duration, options)
}
}
Loading