Skip to content
This repository has been archived by the owner on Mar 7, 2023. It is now read-only.

Conditions #532

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
48 changes: 48 additions & 0 deletions src/conditions/Condition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-disable semi */
class Condition {
constructor (f) {
this.test = f
}

and (otherCondition) {
return Condition.of((env) => {
let r1 = this.test(env)
if (!r1.passed) return r1

let r2 = otherCondition.test(env)
if (!r2.passed) return r2

let reason = r1.reason.concat(r2.reason)
return { passed: true, reason }
})
}

andNot (otherCondition) {
return this.and(otherCondition.negated())
}

or (otherCondition) {
return Condition.of((env) => {
let r = this.test(env)
return r.passed ? r : otherCondition.test(env)
})
}

negated () {
return Condition.of((env) => {
let { passed, reason } = this.test(env)
return { passed: !passed, reason }
})
}

static empty () {
let id = { passed: true, reason: [] }
return Condition.of(() => id)
}

static of (f) {
return new Condition(f)
}
}

module.exports = Condition
45 changes: 45 additions & 0 deletions src/conditions/Environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-disable semi */
const { is, assoc } = require('ramda')

class Environment {
constructor (value) {
this._env = value
}

set (key, getter) {
if (!is(Function, getter)) {
throw new Error('Environment getter must be a function')
}
let value = assoc(key, getter, this._env)
return new Environment(value)
}

has (key) {
return this._env[key] != null
}

get (key) {
if (!this.has(key)) {
throw new Error(`Environment does not contain key '${key}'`)
}
return this._env[key]()
}

static empty () {
return new Environment({})
}

static get GUID () {
return 'guid'
}

static get WALLET_OPTIONS () {
return 'wallet-options'
}

static get ACCOUNT_INFO () {
return 'account-info'
}
}

module.exports = Environment
62 changes: 62 additions & 0 deletions src/conditions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* eslint-disable semi */
const H = require('../helpers')
const Condition = require('./Condition')
const Env = require('./Environment')

const passedWithReason = (passed, reason) => ({
passed, reason: [passed ? reason : 'not_' + reason]
})

exports.isInRolloutGroup = (feature) => Condition.of((env) => {
let guid = env.get(Env.GUID)
let options = env.get(Env.WALLET_OPTIONS)[feature]

let passed = (
H.isStringHashInFraction(guid, options.rolloutFraction)
)

return passedWithReason(passed, 'in_rollout_group')
})

exports.isInStateWhitelist = (feature) => Condition.of((env) => {
let accountInfo = env.get(Env.ACCOUNT_INFO)
let options = env.get(Env.WALLET_OPTIONS)[feature]

let passed = (
accountInfo.stateCodeGuess == null ||
options.statesWhitelist === '*' ||
options.statesWhitelist.indexOf(accountInfo.stateCodeGuess) > -1
)

return passedWithReason(passed, 'in_state_whitelist')
})

exports.isInCountryWhitelist = (feature) => Condition.of((env) => {
let accountInfo = env.get(Env.ACCOUNT_INFO)
let options = env.get(Env.WALLET_OPTIONS)[feature]

let passed = (
accountInfo.countryCodeGuess == null ||
options.countries === '*' ||
options.countries.indexOf(accountInfo.countryCodeGuess) > -1
)

return passedWithReason(passed, 'in_country_whitelist')
})

exports.isInCountryBlacklist = (feature) => Condition.of((env) => {
let accountInfo = env.get(Env.ACCOUNT_INFO)
let options = env.get(Env.WALLET_OPTIONS)[feature]

let passed = (
accountInfo.countryCodeGuess == null ||
options.countriesBlacklist.indexOf(accountInfo.countryCodeGuess) > -1
)

return passedWithReason(passed, 'in_country_blacklist')
})

exports.isUsingTestnet = Condition.of((env) => {
let options = env.get(Env.WALLET_OPTIONS)
return passedWithReason(options.network === 'testnet', 'using_testnet')
})
10 changes: 10 additions & 0 deletions src/eth/eth-wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const EthHd = require('ethereumjs-wallet/hdkey');
const { construct } = require('ramda');
const { isPositiveNumber, asyncOnce, dedup } = require('../helpers');
const API = require('../api');
const conditions = require('../conditions');
const Condition = require('../conditions/Condition');
const EthTxBuilder = require('./eth-tx-builder');
const EthAccount = require('./eth-account');
const EthSocket = require('./eth-socket');
Expand All @@ -11,6 +13,10 @@ const EthWalletTx = require('./eth-wallet-tx');
const METADATA_TYPE_ETH = 5;
const DERIVATION_PATH = "m/44'/60'/0'/0";

const ethCond = Condition.empty()
.andNot(conditions.isUsingTestnet)
.and(conditions.isInRolloutGroup('eth'));

class EthWallet {
constructor (wallet, metadata) {
this._wallet = wallet;
Expand All @@ -24,6 +30,10 @@ class EthWallet {
this.sync = asyncOnce(this.sync.bind(this), 250);
}

userHasAccess () {
return ethCond.test(/* where to get env from? */);
}

get wei () {
return this.defaultAccount ? this.defaultAccount.wei : null;
}
Expand Down
80 changes: 80 additions & 0 deletions tests/conditions/Condition.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable semi */
const Condition = require('../../src/conditions/Condition');

describe('Condition', () => {
const passed = Condition.of(() => ({ passed: true, reason: ['cond_passing'] }))
const failed = Condition.of(() => ({ passed: false, reason: ['cond_failing'] }))

describe('#empty', () => {
it('should create an empty condition', () => {
let c = Condition.empty()
expect(c.test()).toEqual({ passed: true, reason: [] })
})
})

describe('#of', () => {
it('should create a new condition', () => {
let c = Condition.of(() => ({ passed: false, reason: ['reason'] }))
expect(c.test()).toEqual({ passed: false, reason: ['reason'] })
})
})

describe('.and', () => {
it('should create a composite condition', () => {
let c = Condition.empty().and(failed)
expect(c.test()).toEqual(failed.test())
})

it('should include all reasons for a passing condition', () => {
let c = passed.and(passed)
expect(c.test()).toEqual({ passed: true, reason: ['cond_passing', 'cond_passing'] })
})

it('should short-circuit on a failed first condition', () => {
let spy = jasmine.createSpy('test cond')
let c = failed.and(Condition.of(() => spy()))
expect(c.test()).toEqual(failed.test())
expect(spy).not.toHaveBeenCalled()
})
})

describe('.andNot', () => {
it('should create a composite negated condition', () => {
let c = Condition.empty().andNot(passed)
expect(c.test()).toEqual({ passed: false, reason: ['cond_passing'] })
})
})

describe('.or', () => {
it('should create a passing condition if one branch passes', () => {
let c1 = passed.or(failed)
let c2 = failed.or(passed)
expect(c1.test()).toEqual(passed.test())
expect(c2.test()).toEqual(passed.test())
})

it('should create a failing condition if both branches fail', () => {
let c = failed.or(failed)
expect(c.test()).toEqual(failed.test())
})

it('should short-circuit on a passed first condition', () => {
let spy = jasmine.createSpy('test cond')
let c = passed.or(Condition.of(() => spy()))
expect(c.test()).toEqual(passed.test())
expect(spy).not.toHaveBeenCalled()
})
})

describe('.negated', () => {
it('should negate a passing condition', () => {
let c = passed.negated()
expect(c.test()).toEqual({ passed: false, reason: ['cond_passing'] })
})

it('should negate a failing condition', () => {
let c = failed.negated()
expect(c.test()).toEqual({ passed: true, reason: ['cond_failing'] })
})
})
})
48 changes: 48 additions & 0 deletions tests/conditions/Environment.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-disable semi */
const Environment = require('../../src/conditions/Environment');

describe('Environment', () => {
const env = Environment.empty()

describe('.set', () => {
it('should set a property getter', () => {
let e = env.set('prop', () => 'val')
expect(e.get('prop')).toEqual('val')
})

it('should require that the getter is a function', () => {
let fail = () => env.set('prop', 'val')
expect(fail).toThrow()
})
})

describe('.has', () => {
it('should be true when the env property exists', () => {
let e = env.set('prop', () => 'val')
expect(e.has('prop')).toEqual(true)
})

it('should be false when the env property does not exist', () => {
expect(env.has('prop')).toEqual(false)
})

it('should not call the prop getter', () => {
let spy = jasmine.createSpy('getter')
let e = env.set('prop', spy)
expect(e.has('prop')).toEqual(true)
expect(spy).not.toHaveBeenCalled()
})
})

describe('.get', () => {
it('should evaluate the env getter', () => {
let e = env.set('prop', () => 'val')
expect(e.get('prop')).toEqual('val')
})

it('should require that the getter exists', () => {
let fail = () => env.get('prop')
expect(fail).toThrow()
})
})
})
Loading