├── .nvmrc ├── .eslintignore ├── contracts ├── abis │ ├── DSExec.json │ ├── DSMath.json │ ├── GemPit.json │ ├── SaiTubEvents.json │ ├── DSAuthEvents.json │ ├── DSAuthority.json │ ├── DSSpellBook.json │ ├── DSGuardFactory.json │ ├── DSNote.json │ ├── DSGuardEvents.json │ ├── ERC20Events.json │ ├── FakePerson.json │ ├── ProxyRegistry.json │ ├── DSProxyFactory.json │ ├── DSAuth.json │ ├── DSThing.json │ ├── DSSpell.json │ ├── Target.json │ ├── DSStop.json │ ├── ERC20.json │ ├── DSTokenBase.json │ ├── DSValue.json │ ├── DSProxy.json │ ├── DSGuard.json │ ├── SaiVox.json │ ├── DSRoles.json │ ├── SaiTop.json │ ├── SaiMom.json │ ├── SaiProxyCreateAndExecute.json │ └── DSChiefApprovals.json ├── tokens.js ├── contracts.js ├── addresses │ ├── kovan.json │ ├── mainnet.json │ └── testnet.json └── abis.js ├── src ├── bundle │ ├── index.js │ └── index.html ├── exchanges │ ├── orderStyle.js │ └── oasis │ │ ├── OasisOrder.js │ │ └── OasisExchangeService.js ├── eth │ ├── TransactionState.js │ ├── web3 │ │ └── ProviderType.js │ ├── TransactionTransitions.js │ ├── tokens │ │ ├── WethToken.js │ │ ├── PethToken.js │ │ ├── Erc20Token.js │ │ └── EtherToken.js │ ├── TokenConversionService.js │ ├── AllowanceService.js │ ├── GasEstimatorService.js │ ├── NonceService.js │ ├── accounts │ │ ├── factories.js │ │ └── setup.js │ ├── smartContract │ │ └── wrapContract.js │ ├── PriceService.js │ ├── Cdp.js │ ├── EthereumTokenService.js │ ├── TransactionLifeCycle.js │ ├── AccountsService.js │ └── TransactionManager.js ├── config │ ├── presets │ │ ├── http.json │ │ ├── test.json │ │ ├── browser.json │ │ ├── kovan.json │ │ └── mainnet.json │ ├── index.js │ ├── DefaultServiceProvider.js │ ├── ServiceProvider.js │ └── ConfigFactory.js ├── core │ ├── ServiceState.js │ ├── LocalService.js │ ├── PublicService.js │ ├── PrivateService.js │ ├── ServiceType.js │ ├── StateMachine.js │ ├── Container.js │ └── ServiceManager.js ├── utils │ ├── constants.js │ ├── Web3ServiceList.js │ ├── conversion.js │ ├── events │ │ ├── NullEventService.js │ │ ├── helpers.js │ │ ├── EventService.js │ │ └── EventEmitter.js │ ├── loggers │ │ ├── ConsoleLogger.js │ │ ├── NullLogger.js │ │ └── BunyanLogger.js │ ├── CacheService.js │ ├── TimerService.js │ └── index.js ├── index.js └── Maker.js ├── scripts ├── build-frontend.sh ├── run-testchain.sh ├── install-testchain-outputs.sh ├── install-dapptools.sh ├── build-backend.sh ├── set-polling-interval.sh └── showServiceDependencies.js ├── .babelrc ├── .gitmodules ├── .gitignore ├── test ├── setup-global.js ├── setup-test.js ├── config │ ├── index.spec.js │ ├── DefaultServiceProvider.spec.js │ └── ConfigFactory.spec.js ├── helpers │ ├── setupAllowances.js │ ├── ganache.js │ ├── serviceBuilders.js │ ├── TestAccountProvider.spec.js │ └── TestAccountProvider.js ├── utils │ ├── loggers │ │ ├── BunyanLogger.spec.js │ │ └── NullLogger.spec.js │ ├── conversion.spec.js │ ├── events │ │ └── NullEventService.spec.js │ ├── index.spec.js │ └── TimerService.spec.js ├── core │ ├── LocalService.spec.js │ ├── PublicService.spec.js │ └── PrivateService.spec.js ├── eth │ ├── tokens │ │ ├── WethToken.spec.js │ │ ├── PethToken.spec.js │ │ └── Erc20Token.spec.js │ ├── EthereumTokenService.spec.js │ ├── EthereumCdpService.spec.js │ ├── NonceService.spec.js │ ├── PriceService.spec.js │ ├── TokenConversionService.spec.js │ ├── AllowanceService.spec.js │ ├── SmartContractService.spec.js │ └── TransactionObject.spec.js └── Maker.spec.js ├── .travis.yml ├── .eslintrc ├── LICENSE ├── webpack.config.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.scss -------------------------------------------------------------------------------- /contracts/abis/DSExec.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /contracts/abis/DSMath.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /src/bundle/index.js: -------------------------------------------------------------------------------- 1 | import Maker from '../index'; 2 | window.Maker = Maker; 3 | -------------------------------------------------------------------------------- /scripts/build-frontend.sh: -------------------------------------------------------------------------------- 1 | rm -rf dist 2 | webpack --env=prod --progress --profile --colors 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"] 7 | } 8 | -------------------------------------------------------------------------------- /src/exchanges/orderStyle.js: -------------------------------------------------------------------------------- 1 | const enums = { 2 | market: 'market', 3 | limit: 'limit' 4 | }; 5 | export default enums; 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "testchain"] 2 | path = testchain 3 | url = https://github.com/makerdao/testchain 4 | branch = dai.js 5 | -------------------------------------------------------------------------------- /contracts/tokens.js: -------------------------------------------------------------------------------- 1 | export default { 2 | DAI: 'DAI', 3 | MKR: 'MKR', 4 | WETH: 'WETH', 5 | PETH: 'PETH', 6 | ETH: 'ETH' 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/run-testchain.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | CWD="${0%/*}" 5 | 6 | $CWD/set-polling-interval.sh 7 | $CWD/../testchain/scripts/launch $@ 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | node_modules/ 4 | .idea/ 5 | *.iml 6 | npm-debug.log 7 | *.sw* 8 | /out 9 | .DS_Store 10 | ganache.out 11 | package-lock.json -------------------------------------------------------------------------------- /contracts/abis/GemPit.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"gem","type":"address"}],"name":"burn","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /contracts/abis/SaiTubEvents.json: -------------------------------------------------------------------------------- 1 | [{"anonymous":false,"inputs":[{"indexed":true,"name":"lad","type":"address"},{"indexed":false,"name":"cup","type":"bytes32"}],"name":"LogNewCup","type":"event"}] -------------------------------------------------------------------------------- /src/bundle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dai.js script tag include demo 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/eth/TransactionState.js: -------------------------------------------------------------------------------- 1 | const enums = { 2 | initialized: 'initialized', 3 | pending: 'pending', 4 | mined: 'mined', 5 | error: 'error', 6 | finalized: 'finalized' 7 | }; 8 | export default enums; 9 | -------------------------------------------------------------------------------- /src/config/presets/http.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3": { 3 | "provider": { 4 | "type": "HTTP" 5 | }, 6 | "transactionSettings": { 7 | "gasLimit": 4000000 8 | } 9 | }, 10 | "exchange": "OasisExchangeService" 11 | } 12 | -------------------------------------------------------------------------------- /src/config/presets/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3": { 3 | "provider": { 4 | "type": "TEST" 5 | }, 6 | "transactionSettings": { 7 | "gasLimit": 4000000 8 | } 9 | }, 10 | "exchange": "OasisExchangeService" 11 | } 12 | -------------------------------------------------------------------------------- /src/config/presets/browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3": { 3 | "provider": { 4 | "type": "BROWSER" 5 | }, 6 | "transactionSettings": { 7 | "gasLimit": 4000000 8 | } 9 | }, 10 | "exchange": "OasisExchangeService" 11 | } 12 | -------------------------------------------------------------------------------- /test/setup-global.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | /* eslint-disable */ 3 | process.on('unhandledRejection', err => { 4 | console.error('Unhandled rejection is:', err); 5 | }); 6 | 7 | console.log('\nInstalled unhandledRejection logger.'); 8 | }; 9 | -------------------------------------------------------------------------------- /contracts/abis/DSAuthEvents.json: -------------------------------------------------------------------------------- 1 | [{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /contracts/abis/DSAuthority.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"sig","type":"bytes4"}],"name":"canCall","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"}] -------------------------------------------------------------------------------- /contracts/abis/DSSpellBook.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"whom","type":"address"},{"name":"mana","type":"uint256"},{"name":"data","type":"bytes"}],"name":"make","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /src/config/presets/kovan.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3": { 3 | "provider": { 4 | "type": "INFURA", 5 | "network": "kovan" 6 | }, 7 | "transactionSettings": { 8 | "gasLimit": 4000000 9 | } 10 | }, 11 | "exchange": "OasisExchangeService" 12 | } 13 | -------------------------------------------------------------------------------- /src/config/presets/mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "web3": { 3 | "provider": { 4 | "type": "INFURA", 5 | "network": "mainnet" 6 | }, 7 | "transactionSettings": { 8 | "gasLimit": 4000000 9 | } 10 | }, 11 | "exchange": "OasisExchangeService" 12 | } 13 | -------------------------------------------------------------------------------- /src/eth/web3/ProviderType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | INFURA: 'INFURA', 3 | HTTP: 'HTTP', 4 | TEST: 'TEST', 5 | 6 | // a browser provider is one that comes from the browser, i.e. `window.web3`, 7 | // or from MetaMask's new "postMessage" method of requesting a provider. 8 | BROWSER: 'BROWSER' 9 | }; 10 | -------------------------------------------------------------------------------- /src/core/ServiceState.js: -------------------------------------------------------------------------------- 1 | const ServiceState = { 2 | CREATED: 'CREATED', 3 | INITIALIZING: 'INITIALIZING', 4 | OFFLINE: 'OFFLINE', 5 | CONNECTING: 'CONNECTING', 6 | ONLINE: 'ONLINE', 7 | AUTHENTICATING: 'AUTHENTICATING', 8 | READY: 'READY', 9 | ERROR: 'ERROR' 10 | }; 11 | 12 | export default ServiceState; 13 | -------------------------------------------------------------------------------- /scripts/install-testchain-outputs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | CWD=`dirname $0` 5 | CONTRACTS=$CWD/../contracts 6 | SOURCE=${1:-$CWD/../testchain} 7 | 8 | for file in $SOURCE/out/*.abi; do 9 | cp $file $CONTRACTS/abis/$(basename $file .abi).json 10 | done 11 | 12 | cp $SOURCE/out/addresses.json $CONTRACTS/addresses/testnet.json 13 | -------------------------------------------------------------------------------- /contracts/abis/DSGuardFactory.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"isGuard","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"newGuard","outputs":[{"name":"guard","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"}] -------------------------------------------------------------------------------- /contracts/abis/DSNote.json: -------------------------------------------------------------------------------- 1 | [{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"}] -------------------------------------------------------------------------------- /scripts/install-dapptools.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # run this file with "source" or "." so it can affect the environment 3 | 4 | set -e 5 | 6 | sudo mkdir -m 0755 /nix && sudo chown travis /nix 7 | sudo mount -o bind /home/travis/build/makerdao/dai.js/nix /nix 8 | curl https://dapp.tools/install | sh 9 | . /home/travis/.nix-profile/etc/profile.d/nix.sh 10 | -------------------------------------------------------------------------------- /contracts/abis/DSGuardEvents.json: -------------------------------------------------------------------------------- 1 | [{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"bytes32"},{"indexed":true,"name":"dst","type":"bytes32"},{"indexed":true,"name":"sig","type":"bytes32"}],"name":"LogPermit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"bytes32"},{"indexed":true,"name":"dst","type":"bytes32"},{"indexed":true,"name":"sig","type":"bytes32"}],"name":"LogForbid","type":"event"}] -------------------------------------------------------------------------------- /contracts/abis/ERC20Events.json: -------------------------------------------------------------------------------- 1 | [{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"}] -------------------------------------------------------------------------------- /src/core/LocalService.js: -------------------------------------------------------------------------------- 1 | import ServiceType from './ServiceType'; 2 | import ServiceBase from './ServiceBase'; 3 | 4 | /** 5 | * 6 | */ 7 | class LocalService extends ServiceBase { 8 | /** 9 | * @param {string} name 10 | * @param {string[]} dependencies 11 | */ 12 | constructor(name, dependencies = []) { 13 | super(ServiceType.LOCAL, name, dependencies); 14 | } 15 | } 16 | 17 | export default LocalService; 18 | -------------------------------------------------------------------------------- /src/core/PublicService.js: -------------------------------------------------------------------------------- 1 | import ServiceType from './ServiceType'; 2 | import ServiceBase from './ServiceBase'; 3 | 4 | /** 5 | * 6 | */ 7 | class PublicService extends ServiceBase { 8 | /** 9 | * @param {string} name 10 | * @param {string[]} dependencies 11 | */ 12 | constructor(name, dependencies = []) { 13 | super(ServiceType.PUBLIC, name, dependencies); 14 | } 15 | } 16 | 17 | export default PublicService; 18 | -------------------------------------------------------------------------------- /src/core/PrivateService.js: -------------------------------------------------------------------------------- 1 | import ServiceType from './ServiceType'; 2 | import ServiceBase from './ServiceBase'; 3 | 4 | /** 5 | * 6 | */ 7 | class PrivateService extends ServiceBase { 8 | /** 9 | * @param {string} name 10 | * @param {string[]} dependencies 11 | */ 12 | constructor(name, dependencies = []) { 13 | super(ServiceType.PRIVATE, name, dependencies); 14 | } 15 | } 16 | 17 | export default PrivateService; 18 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | 3 | export const WEI = new BigNumber('1e18'); 4 | export const WAD = new BigNumber('1e18'); 5 | export const RAY = new BigNumber('1e27'); 6 | 7 | export const UINT256_MAX = 8 | '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; 9 | 10 | export const AccountType = { 11 | PROVIDER: 'provider', 12 | PRIVATE_KEY: 'privateKey', 13 | BROWSER: 'browser' 14 | }; 15 | -------------------------------------------------------------------------------- /contracts/contracts.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SAI_PIP: 'SAI_PIP', 3 | SAI_PEP: 'SAI_PEP', 4 | SAI_PIT: 'SAI_PIT', 5 | SAI_SIN: 'SAI_SIN', 6 | SAI_DAD: 'SAI_DAD', 7 | SAI_MOM: 'SAI_MOM', 8 | SAI_VOX: 'SAI_VOX', 9 | SAI_TUB: 'SAI_TUB', 10 | SAI_TAP: 'SAI_TAP', 11 | SAI_TOP: 'SAI_TOP', 12 | MAKER_OTC: 'MAKER_OTC', 13 | SAI_PROXY: 'SAI_PROXY', 14 | PROXY_REGISTRY: 'PROXY_REGISTRY', 15 | DS_PROXY_FACTORY: 'DS_PROXY_FACTORY', 16 | DS_PROXY: 'DS_PROXY' 17 | }; 18 | -------------------------------------------------------------------------------- /test/setup-test.js: -------------------------------------------------------------------------------- 1 | import Web3ServiceList from '../src/utils/Web3ServiceList'; 2 | import { takeSnapshot, restoreSnapshot } from './helpers/ganache'; 3 | 4 | beforeEach(() => { 5 | jest.setTimeout(10000); 6 | }); 7 | 8 | afterEach(() => { 9 | return Web3ServiceList.disconnectAll(); 10 | }); 11 | 12 | let snapshotId; 13 | 14 | beforeAll(async () => { 15 | snapshotId = await takeSnapshot(); 16 | }); 17 | 18 | afterAll(async () => { 19 | await restoreSnapshot(snapshotId); 20 | }); 21 | -------------------------------------------------------------------------------- /test/config/index.spec.js: -------------------------------------------------------------------------------- 1 | import { standardizeConfig } from '../../src/config'; 2 | 3 | test('accepts a constructor', () => { 4 | class FakeService {} 5 | const config = standardizeConfig('timer', FakeService); 6 | expect(config).toEqual([FakeService, {}]); 7 | }); 8 | 9 | test('accepts a constructor + settings pair', () => { 10 | class FakeService {} 11 | const config = standardizeConfig('timer', [FakeService, {foo: 3}]); 12 | expect(config).toEqual([FakeService, {foo: 3}]); 13 | }); 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Maker from './Maker'; 2 | import { currencies } from './eth/Currency'; 3 | import LocalService from './core/LocalService'; 4 | import PrivateService from './core/PrivateService'; 5 | import PublicService from './core/PublicService'; 6 | 7 | for (let symbol in currencies) { 8 | Maker[symbol] = currencies[symbol]; 9 | } 10 | 11 | Maker.LocalService = LocalService; 12 | Maker.PrivateService = PrivateService; 13 | Maker.PublicService = PublicService; 14 | 15 | module.exports = Maker; 16 | -------------------------------------------------------------------------------- /src/utils/Web3ServiceList.js: -------------------------------------------------------------------------------- 1 | class Web3ServiceList { 2 | constructor() { 3 | this._list = []; 4 | } 5 | 6 | push(service) { 7 | //put a warning if this list is length 2 or more 8 | this._list.push(service); 9 | } 10 | 11 | disconnectAll() { 12 | return Promise.all(this._list, s => s.manager()._disconnect()).then( 13 | () => (this._list = []) 14 | ); 15 | } 16 | } 17 | 18 | // eslint-disable-next-line 19 | const l = new Web3ServiceList(); 20 | export default l; 21 | -------------------------------------------------------------------------------- /scripts/build-backend.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ "$1" = "dirty" ]; then 6 | echo "Dirty mode: not removing previous build files." 7 | else 8 | rm -rf dist 9 | fi 10 | 11 | babel -q contracts --out-dir ./dist/contracts 12 | babel -q contracts/addresses --out-dir ./dist/contracts/addresses 13 | babel -q src --out-dir ./dist/src 14 | 15 | copyfiles \ 16 | README.md \ 17 | LICENSE \ 18 | package.json \ 19 | contracts/abis/* \ 20 | src/config/presets/* \ 21 | contracts/addresses/* \ 22 | dist 23 | -------------------------------------------------------------------------------- /contracts/abis/FakePerson.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[],"name":"sai","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"cash","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tap","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_tap","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}] -------------------------------------------------------------------------------- /scripts/set-polling-interval.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # speed up ethers.js polling so tests finish faster 3 | # run this from the project root 4 | 5 | CWD="${0%/*}" 6 | INTERVAL=${1:-50} 7 | 8 | function sed_inplace { 9 | # sed's -i argument behaves differently on macOS, hence this hack 10 | sed -i.bak "$1" $2 && rm $2.bak 11 | } 12 | 13 | echo Setting ethers.js polling interval to $INTERVAL ms. 14 | sed_inplace "s/var pollingInterval = [0-9]*/var pollingInterval = $INTERVAL/" \ 15 | $CWD/../node_modules/ethers/providers/provider.js 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: node_js 4 | node_js: 5 | - '8' 6 | before_cache: 7 | - nix-collect-garbage -d 8 | cache: 9 | directories: 10 | - node_modules 11 | - nix 12 | timeout: 9000 13 | before_install: 14 | - . scripts/install-dapptools.sh 15 | - npm i -g npm@latest 16 | install: 17 | - npm install 18 | - npm run build:backend 19 | - npm run build:frontend 20 | before_script: 21 | - npm install -g codecov 22 | script: 23 | - npm run coverage 24 | - codecov 25 | after_install: 26 | - cat /home/travis/.npm/_logs/2018-02-*.log 27 | -------------------------------------------------------------------------------- /src/utils/conversion.js: -------------------------------------------------------------------------------- 1 | import { utils as ethersUtils } from 'ethers'; 2 | 3 | export function numberToBytes32(num) { 4 | const bn = ethersUtils.bigNumberify(num); 5 | return ethersUtils.hexlify(ethersUtils.padZeros(bn, 32)); 6 | } 7 | 8 | export function bytes32ToNumber(bytes32) { 9 | return ethersUtils.bigNumberify(bytes32).toNumber(); 10 | } 11 | 12 | export function stringToBytes32(text) { 13 | var data = ethersUtils.toUtf8Bytes(text); 14 | if (data.length > 32) { 15 | throw new Error('too long'); 16 | } 17 | data = ethersUtils.padZeros(data, 32); 18 | return ethersUtils.hexlify(data); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/events/NullEventService.js: -------------------------------------------------------------------------------- 1 | import LocalService from '../../core/LocalService'; 2 | 3 | const _ = () => {}; 4 | 5 | export default class NullEventService extends LocalService { 6 | /** 7 | * @param {string} name 8 | */ 9 | constructor(name = 'event') { 10 | super(name); 11 | } 12 | 13 | on() {} 14 | emit() {} 15 | ping() {} 16 | removeListener() {} 17 | registerPollEvents() {} 18 | buildEmitter() { 19 | return { 20 | emit: _, 21 | on: _, 22 | removeListener: _, 23 | registerPollEvents: _, 24 | ping: _, 25 | dispose: _ 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/helpers/setupAllowances.js: -------------------------------------------------------------------------------- 1 | import { buildTestContainer } from './serviceBuilders'; 2 | import tokens from '../../contracts/tokens'; 3 | 4 | export default async function setupAllowances(tokenList = []) { 5 | const container = buildTestContainer({ cdp: true, token: true }); 6 | const tokenService = container.service('token'); 7 | const cdpService = container.service('cdp'); 8 | await tokenService.manager().authenticate(); 9 | 10 | for (let symbol of tokenList) { 11 | const token = tokenService.getToken(tokens[symbol.toUpperCase()]); 12 | await token.approve(cdpService._tubContract().address, '0'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/utils/loggers/BunyanLogger.spec.js: -------------------------------------------------------------------------------- 1 | import ServiceManager from '../../../src/core/ServiceManager'; 2 | import BunyanLogger from '../../../src/utils/loggers/BunyanLogger'; 3 | 4 | test('should correctly log info messages and service lifecycle events', () => { 5 | const log = new BunyanLogger(), 6 | svc = new ServiceManager('MyService', ['log']) 7 | .inject('log', log) 8 | .createService(); 9 | 10 | //@todo: find a way to properly test log output 11 | //log.info('Test 1'); 12 | //log.info({ hello: 'Test 2' }, 'Text is %hello'); 13 | //log.serviceLogger(svc); 14 | 15 | svc.manager().initialize(); 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/loggers/ConsoleLogger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import PrivateService from '../../core/PrivateService'; 3 | 4 | export default class ConsoleLogger extends PrivateService { 5 | /** 6 | * @param {string} name 7 | */ 8 | constructor(name = 'log') { 9 | super(name); 10 | } 11 | 12 | debug(...args) { 13 | console.log(...args); 14 | } 15 | 16 | info(...args) { 17 | console.info(...args); 18 | } 19 | 20 | warn(...args) { 21 | console.warn(...args); 22 | } 23 | 24 | error(...args) { 25 | console.error(...args); 26 | } 27 | 28 | trace(...args) { 29 | console.trace(...args); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/eth/TransactionTransitions.js: -------------------------------------------------------------------------------- 1 | import transactionStatus from '../eth/TransactionState'; 2 | 3 | const TransactionType = { 4 | oasis: 'oasis', 5 | transaction: 'transaction' 6 | }; 7 | 8 | const transactionLifeCycle = { 9 | initialized: [transactionStatus.pending, transactionStatus.error], 10 | pending: [transactionStatus.error, transactionStatus.mined], 11 | mined: [transactionStatus.finalized, transactionStatus.error], 12 | finalized: [], 13 | error: [] 14 | }; 15 | 16 | const transactionTypeTransitions = { 17 | transaction: transactionLifeCycle 18 | }; 19 | 20 | export { TransactionType as default, transactionTypeTransitions }; 21 | -------------------------------------------------------------------------------- /src/eth/tokens/WethToken.js: -------------------------------------------------------------------------------- 1 | import Erc20Token from './Erc20Token'; 2 | import { ETH } from '../Currency'; 3 | 4 | export default class WethToken extends Erc20Token { 5 | constructor(contract, web3Service, decimals) { 6 | super(contract, web3Service, decimals, 'WETH'); 7 | } 8 | 9 | name() { 10 | return this._contract.name(); 11 | } 12 | 13 | deposit(amount, unit = ETH) { 14 | return this._contract.deposit({ 15 | value: this._valueForContract(amount, unit) 16 | }); 17 | } 18 | 19 | withdraw(amount, unit = ETH) { 20 | const value = this._valueForContract(amount, unit); 21 | return this._contract.withdraw(value); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/CacheService.js: -------------------------------------------------------------------------------- 1 | import LocalService from '../core/LocalService'; 2 | 3 | export default class CacheService extends LocalService { 4 | constructor(name = 'cache') { 5 | super(name); 6 | } 7 | 8 | initialize(settings = {}) { 9 | if (settings.storage) { 10 | this._storage = settings.storage; 11 | } 12 | } 13 | 14 | isEnabled() { 15 | return !!this._storage; 16 | } 17 | 18 | has(key) { 19 | return !!this._storage && key in this._storage; 20 | } 21 | 22 | fetch(key) { 23 | return this._storage ? this._storage[key] : undefined; 24 | } 25 | 26 | store(key, value) { 27 | if (this._storage) this._storage[key] = value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/core/LocalService.spec.js: -------------------------------------------------------------------------------- 1 | import ServiceBase from '../../src/core/ServiceBase'; 2 | import LocalService from '../../src/core/LocalService'; 3 | import ServiceType from '../../src/core/ServiceType'; 4 | 5 | test('should be a service of type LOCAL, with the provided name and dependencies', () => { 6 | const service = new LocalService('MyName', ['X', 'Y', 'Z']); 7 | 8 | expect(service).toBeInstanceOf(ServiceBase); 9 | expect(service.manager().type()).toBe(ServiceType.LOCAL); 10 | expect(service.manager().name()).toBe('MyName'); 11 | expect(service.manager().dependencies()).toEqual(['X', 'Y', 'Z']); 12 | 13 | expect(new LocalService('MyName').manager().dependencies()).toEqual([]); 14 | }); 15 | -------------------------------------------------------------------------------- /test/utils/conversion.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | numberToBytes32, 3 | bytes32ToNumber, 4 | stringToBytes32 5 | } from '../../src/utils/conversion'; 6 | 7 | test('numberToBytes32', () => { 8 | expect(numberToBytes32(92)).toBe( 9 | '0x000000000000000000000000000000000000000000000000000000000000005c' 10 | ); 11 | }); 12 | 13 | test('bytes32ToNumber', () => { 14 | const bytes32 = 15 | '0x000000000000000000000000000000000000000000000000000000000000005c'; 16 | expect(bytes32ToNumber(bytes32)).toBe(92); 17 | }); 18 | 19 | test('stringToBytes32', () => { 20 | expect(stringToBytes32('hello')).toBe( 21 | '0x00000000000000000000000000000000000000000000000000000068656c6c6f' 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /test/core/PublicService.spec.js: -------------------------------------------------------------------------------- 1 | import ServiceBase from '../../src/core/ServiceBase'; 2 | import PublicService from '../../src/core/PublicService'; 3 | import ServiceType from '../../src/core/ServiceType'; 4 | 5 | test('should be a service of type PUBLIC, with the provided name and dependencies', () => { 6 | const service = new PublicService('MyName', ['X', 'Y', 'Z']); 7 | 8 | expect(service).toBeInstanceOf(ServiceBase); 9 | expect(service.manager().type()).toBe(ServiceType.PUBLIC); 10 | expect(service.manager().name()).toBe('MyName'); 11 | expect(service.manager().dependencies()).toEqual(['X', 'Y', 'Z']); 12 | 13 | expect(new PublicService('MyName').manager().dependencies()).toEqual([]); 14 | }); 15 | -------------------------------------------------------------------------------- /test/core/PrivateService.spec.js: -------------------------------------------------------------------------------- 1 | import ServiceBase from '../../src/core/ServiceBase'; 2 | import PrivateService from '../../src/core/PrivateService'; 3 | import ServiceType from '../../src/core/ServiceType'; 4 | 5 | test('should be a service of type PRIVATE, with the provided name and dependencies', () => { 6 | const service = new PrivateService('MyName', ['X', 'Y', 'Z']); 7 | 8 | expect(service).toBeInstanceOf(ServiceBase); 9 | expect(service.manager().type()).toBe(ServiceType.PRIVATE); 10 | expect(service.manager().name()).toBe('MyName'); 11 | expect(service.manager().dependencies()).toEqual(['X', 'Y', 'Z']); 12 | 13 | expect(new PrivateService('MyName').manager().dependencies()).toEqual([]); 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils/loggers/NullLogger.js: -------------------------------------------------------------------------------- 1 | import ServiceManager from '../../core/ServiceManager'; 2 | import LocalService from '../../core/LocalService'; 3 | 4 | const _ = () => {}; 5 | 6 | export default class NullLogger extends LocalService { 7 | /** 8 | * @param {string} name 9 | */ 10 | constructor(name = 'log') { 11 | super(name); 12 | } 13 | 14 | /** 15 | * @param {object} service 16 | * @returns {object} 17 | */ 18 | serviceLogger(service) { 19 | if (!ServiceManager.isValidService(service)) { 20 | throw new Error('Invalid service object'); 21 | } 22 | 23 | return { trace: _, debug: _, info: _, warn: _, error: _, fatal: _ }; 24 | } 25 | 26 | trace() {} 27 | debug() {} 28 | info() {} 29 | warn() {} 30 | error() {} 31 | fatal() {} 32 | } 33 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "jest": true, 7 | "node": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "ecmaVersion": 8, 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "experimentalObjectRestSpread": true 15 | } 16 | }, 17 | "rules": { 18 | "no-console": "off", 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single", 26 | { "avoidEscape": true } 27 | ], 28 | "semi": [ 29 | "error", 30 | "always" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/abis/ProxyRegistry.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"uint256"}],"name":"proxies","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"build","outputs":[{"name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"proxiesCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"owner","type":"address"}],"name":"build","outputs":[{"name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"factory_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}] -------------------------------------------------------------------------------- /test/helpers/ganache.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | const ganacheAddress = 'http://localhost:2000'; 4 | let requestCount = 0; 5 | 6 | function callGanache(method, params = []) { 7 | return fetch(ganacheAddress, { 8 | method: 'POST', 9 | headers: { 10 | Accept: 'application/json', 11 | 'Content-Type': 'application/json' 12 | }, 13 | body: JSON.stringify({ 14 | jsonrpc: '2.0', 15 | method, 16 | params, 17 | id: requestCount++ 18 | }) 19 | }); 20 | } 21 | 22 | export async function takeSnapshot() { 23 | const res = await callGanache('evm_snapshot'); 24 | const { result } = await res.json(); 25 | return parseInt(result, 16); 26 | } 27 | 28 | export async function restoreSnapshot(snapId) { 29 | const res = await callGanache('evm_revert', [snapId]); 30 | return (await res.json()).result; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/TimerService.js: -------------------------------------------------------------------------------- 1 | import LocalService from '../core/LocalService'; 2 | 3 | export default class TimerService extends LocalService { 4 | constructor(name = 'timer') { 5 | super(name); 6 | this._timers = {}; 7 | } 8 | 9 | createTimer(name, duration, repeating, callback) { 10 | this.disposeTimer(name); 11 | this._timers[name] = { 12 | repeating, 13 | id: (repeating ? setInterval : setTimeout)(callback, duration) 14 | }; 15 | } 16 | 17 | disposeTimer(name) { 18 | if (this._timers.hasOwnProperty(name)) { 19 | let timer = this._timers[name]; 20 | (timer.repeating ? clearInterval : clearTimeout)(timer.id); 21 | } 22 | } 23 | 24 | disposeAllTimers() { 25 | for (let name of this.listTimers()) { 26 | this.disposeTimer(name); 27 | } 28 | } 29 | 30 | listTimers() { 31 | return Object.keys(this._timers); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/abis/DSProxyFactory.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"isProxy","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"cache","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"build","outputs":[{"name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"owner","type":"address"}],"name":"build","outputs":[{"name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"sender","type":"address"},{"indexed":true,"name":"owner","type":"address"},{"indexed":false,"name":"proxy","type":"address"},{"indexed":false,"name":"cache","type":"address"}],"name":"Created","type":"event"}] -------------------------------------------------------------------------------- /contracts/abis/DSAuth.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /src/eth/tokens/PethToken.js: -------------------------------------------------------------------------------- 1 | import Erc20Token from './Erc20Token'; 2 | import { WETH } from '../Currency'; 3 | 4 | export default class PethToken extends Erc20Token { 5 | constructor(contract, web3Service, tub) { 6 | super(contract, web3Service, 18, 'PETH'); 7 | this._tub = tub; 8 | } 9 | 10 | join(amount, unit = WETH) { 11 | const value = this._valueForContract(amount, unit); 12 | return this._tub.join(value); 13 | } 14 | 15 | exit(amount, unit = WETH) { 16 | const value = this._valueForContract(amount, unit); 17 | return this._tub.exit(value); 18 | } 19 | 20 | async wrapperRatio() { 21 | return WETH.ray(await this._tub.per()); 22 | } 23 | 24 | async joinPrice(amount, unit = WETH) { 25 | const value = this._valueForContract(amount, unit); 26 | return WETH.wei(await this._tub.ask(value)); 27 | } 28 | 29 | async exitPrice(amount, unit = WETH) { 30 | const value = this._valueForContract(amount, unit); 31 | return WETH.wei(await this._tub.bid(value)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/utils/events/NullEventService.spec.js: -------------------------------------------------------------------------------- 1 | import NullEventService from '../../../src/utils/events/NullEventService'; 2 | 3 | test('should have a valid event service interface', () => { 4 | const nullEventService = new NullEventService(); 5 | expect(nullEventService.on({}, '')).toBeFalsy(); 6 | expect(nullEventService.emit({}, '')).toBeFalsy(); 7 | expect(nullEventService.ping({}, '')).toBeFalsy(); 8 | expect(nullEventService.removeListener({}, '')).toBeFalsy(); 9 | expect(nullEventService.registerPollEvents({}, '')).toBeFalsy(); 10 | expect(nullEventService.buildEmitter({}, '')).toBeDefined(); 11 | }); 12 | 13 | test('buildEmitter() should return new emitter with a valid interface', () => { 14 | const nullEventService = new NullEventService(); 15 | const emitterInstance = nullEventService.buildEmitter(); 16 | expect(emitterInstance.emit({}, '')).toBeFalsy(); 17 | expect(emitterInstance.on({}, '')).toBeFalsy(); 18 | expect(emitterInstance.removeListener({}, '')).toBeFalsy(); 19 | expect(emitterInstance.dispose({}, '')).toBeFalsy(); 20 | }); 21 | -------------------------------------------------------------------------------- /src/eth/TokenConversionService.js: -------------------------------------------------------------------------------- 1 | import PrivateService from '../core/PrivateService'; 2 | import contracts from '../../contracts/contracts'; 3 | import { getCurrency, ETH, PETH, WETH } from './Currency'; 4 | 5 | export default class TokenConversionService extends PrivateService { 6 | constructor(name = 'conversion') { 7 | super(name, ['smartContract', 'token', 'allowance']); 8 | } 9 | 10 | _getToken(token) { 11 | return this.get('token').getToken(token); 12 | } 13 | 14 | convertEthToWeth(amount, unit = ETH) { 15 | return this._getToken(WETH).deposit(getCurrency(amount, unit)); 16 | } 17 | 18 | async convertWethToPeth(amount, unit = WETH) { 19 | const pethToken = this._getToken(PETH); 20 | 21 | await this.get('allowance').requireAllowance( 22 | WETH, 23 | this.get('smartContract').getContractByName(contracts.SAI_TUB).address 24 | ); 25 | return pethToken.join(amount, unit); 26 | } 27 | 28 | async convertEthToPeth(value) { 29 | await this.convertEthToWeth(value); 30 | return this.convertWethToPeth(value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Maker Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/utils/index.spec.js: -------------------------------------------------------------------------------- 1 | import { promisify, getNetworkName } from '../../src/utils'; 2 | 3 | describe('promisify makes async functions return Promises', () => { 4 | test('arguments can be passed and results are resolved', () => { 5 | expect.assertions(1); 6 | 7 | const argVal = 'foobar'; 8 | const promisified = promisify((arg1, cb) => { 9 | cb(null, arg1); 10 | }); 11 | 12 | promisified(argVal).then(val => { 13 | expect(val).toEqual(argVal); 14 | }); 15 | }); 16 | 17 | test('errors are rejected', () => { 18 | expect.assertions(1); 19 | 20 | const errVal = 'foobar'; 21 | const promisified = promisify(cb => { 22 | cb(errVal); 23 | }); 24 | 25 | promisified().catch(val => { 26 | expect(val).toEqual(errVal); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('getNetworkName', () => { 32 | test('should return the name of the matched network', () => { 33 | expect(getNetworkName(42)).toBe('kovan'); 34 | }); 35 | 36 | test('should throw an error if no network is matched', () => { 37 | expect(() => getNetworkName(43)).toThrow('No network with ID 43 found.'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /contracts/addresses/kovan.json: -------------------------------------------------------------------------------- 1 | { 2 | "GEM": "0xd0A1E359811322d97991E03f863a0C30C2cF029C", 3 | "GOV": "0xAaF64BFCC32d0F15873a02163e7E500671a4ffcD", 4 | "PIP": "0xa944bd4b25c9f186a846fd5668941aa3d3b8425f", 5 | "PEP": "0x02998f73fabb52282664094b0ff87741a1ce9030", 6 | "PIT": "0xbd747742b0f1f9791d3e6b85f8797a0cf4fbf10b", 7 | "ADM": "0x74d41Fd874234D9beA31fF6b090Ba1D0b9Dc8785", 8 | "SAI": "0xC4375B7De8af5a38a93548eb8453a498222C4fF2", 9 | "SIN": "0xdcdca4371befceafa069ca1e2afd8b925b69e57b", 10 | "SKR": "0xf4d791139cE033Ad35DB2B2201435fAd668B1b64", 11 | "DAD": "0x6a884c7af48e29a20be9ff04bdde112b5596fcee", 12 | "MOM": "0x72ee9496b0867dfe5e8b280254da55e51e34d27b", 13 | "VOX": "0xbb4339c0ab5b1d9f14bd6e3426444a1e9d86a1d9", 14 | "TUB": "0xa71937147b55Deb8a530C7229C442Fd3F31b7db2", 15 | "TAP": "0xc936749d2d0139174ee0271bd28325074fdbc654", 16 | "TOP": "0x5f00393547561da3030ebf30e52f5dc0d5d3362c", 17 | "MAKER_OTC": "0x8cf1Cab422A0b6b554077A361f8419cDf122a9F9", 18 | "SAI_PROXY": "0xadb7c74bce932fc6c27dda3ac2344707d2fbb0e6", 19 | "PROXY_REGISTRY": "0x64a436ae831c1672ae81f674cab8b6775df3475c", 20 | "DS_PROXY_FACTORY": "0xe11e3b391f7e8bc47247866af32af67dd58dc800" 21 | } 22 | -------------------------------------------------------------------------------- /contracts/abis/DSThing.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /contracts/abis/DSSpell.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[],"name":"data","outputs":[{"name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"cast","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"done","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"mana","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"whom","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"whom_","type":"address"},{"name":"mana_","type":"uint256"},{"name":"data_","type":"bytes"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"}] -------------------------------------------------------------------------------- /contracts/addresses/mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "GEM": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", 3 | "GOV_OLD": "0xC66eA802717bFb9833400264Dd12c2bCeAa34a6d", 4 | "GOV": "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", 5 | "PIP": "0x729D19f657BD0614b4985Cf1D82531c67569197B", 6 | "PEP": "0x99041F808D598B782D5a3e498681C2452A31da08", 7 | "PIT": "0x69076e44a9c70a67d5b79d95795aba299083c275", 8 | "ADM": "0x8E2a84D6adE1E7ffFEe039A35EF5F19F13057152", 9 | "SAI": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359", 10 | "SIN": "0x79f6d0f646706e1261acf0b93dcb864f357d4680", 11 | "SKR": "0xf53ad2c6851052a81b42133467480961b2321c09", 12 | "DAD": "0x315cbb88168396d12e1a255f9cb935408fe80710", 13 | "MOM": "0xf2c5369cffb8ea6284452b0326e326dbfdcb867c", 14 | "VOX": "0x9b0f70df76165442ca6092939132bbaea77f2d7a", 15 | "TUB": "0x448a5065aebb8e423f0896e6c5d525c040f59af3", 16 | "TAP": "0xbda109309f9fafa6dd6a9cb9f1df4085b27ee8ef", 17 | "TOP": "0x9b0ccf7c8994e19f39b2b4cf708e0a7df65fa8a3", 18 | "MAKER_OTC": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", 19 | "SAI_PROXY": "0x190c2cfc69e68a8e8d5e2b9e2b9cc3332caff77b", 20 | "PROXY_REGISTRY": "0x4678f0a6958e4d2bc4f1baf7bc52e8f3564f3fe4", 21 | "DS_PROXY_FACTORY": "0xa26e15c895efc0616177b7c1e7270a4c7d51c997" 22 | } 23 | -------------------------------------------------------------------------------- /contracts/addresses/testnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "GEM": "0x7ba25f791fa76c3ef40ac98ed42634a8bc24c238", 3 | "GOV": "0x1c3ac7216250edc5b9daa5598da0579688b9dbd5", 4 | "PIP": "0xb7092ee7a8c4c85431962662310bbdcd4fd519e9", 5 | "PEP": "0xc0ee05307ae4a5316f34874a3525d10c94b3c217", 6 | "PIT": "0x0000000000000000000000000000000000000123", 7 | "ADM": "0x4986c24c7f752c2ac2d738f1270639dd9e9d7bf5", 8 | "SAI": "0xc226f3cd13d508bc319f4f4290172748199d6612", 9 | "SIN": "0xe9e2b40d676fc998ede8c676d9f529ccbbc13740", 10 | "SKR": "0xa6164a2e88e258a663772ed4912a0865af8f6d06", 11 | "DAD": "0x7b61731911e46da837e3dcd2d8797de684c8ced1", 12 | "MOM": "0x603d52d6ae2b98a49f8f32817ad4effe7e8a2502", 13 | "VOX": "0xe16bf7aafeb33cc921d6d311e0ff33c4faa836dd", 14 | "TUB": "0xe82ce3d6bf40f2f9414c8d01a35e3d9eb16a1761", 15 | "TAP": "0x6896659267c3c9fd055d764327199a98e571e00d", 16 | "TOP": "0x2774031b3898fbe414f929b3223ce1039325e7dc", 17 | "MAKER_OTC": "0x06ef37a95603cb52e2dff4c2b177c84cdb3ce989", 18 | "DS_PROXY_FACTORY": "0x23f67a19dc232835eaeda2075728f8295f54dfca", 19 | "PROXY_REGISTRY": "0x9706786bf567796647d9428076fbc219830116ae", 20 | "DS_PROXY": "0xaff08328e5a586754702f570d70972c43ab82ef8", 21 | "SAI_PROXY": "0xc72b03c37735cf122c27dc352e5f25f75beea389" 22 | } 23 | -------------------------------------------------------------------------------- /test/eth/tokens/WethToken.spec.js: -------------------------------------------------------------------------------- 1 | import { buildTestEthereumTokenService } from '../../helpers/serviceBuilders'; 2 | import TestAccountProvider from '../../helpers/TestAccountProvider'; 3 | import { WETH } from '../../../src/eth/Currency'; 4 | 5 | let tokenService, weth; 6 | 7 | beforeAll(async () => { 8 | tokenService = buildTestEthereumTokenService(); 9 | await tokenService.manager().authenticate(); 10 | weth = tokenService.getToken(WETH); 11 | }); 12 | 13 | test('get WETH allowance of address', async () => { 14 | const allowance = await weth.allowance( 15 | TestAccountProvider.nextAddress(), 16 | TestAccountProvider.nextAddress() 17 | ); 18 | expect(allowance).toEqual(WETH(0)); 19 | }); 20 | 21 | test('token name and symbol are correct', async () => { 22 | expect(await weth._contract.symbol()).toBe('WETH'); 23 | expect(await weth.name()).toBe('Wrapped Ether'); 24 | }); 25 | 26 | test('wrap and unwrap ETH', async () => { 27 | const owner = tokenService.get('web3').currentAccount(); 28 | const balance1 = await weth.balanceOf(owner); 29 | await weth.deposit(0.1); 30 | const balance2 = await weth.balanceOf(owner); 31 | expect(balance1.plus(0.1)).toEqual(balance2); 32 | await weth.withdraw(0.1); 33 | const balance3 = await weth.balanceOf(owner); 34 | expect(balance2.minus(0.1)).toEqual(balance3); 35 | }); 36 | -------------------------------------------------------------------------------- /contracts/abis.js: -------------------------------------------------------------------------------- 1 | import erc20 from './abis/ERC20.json'; 2 | 3 | import dsEthToken from './abis/WETH9.json'; 4 | import dsValue from './abis/DSValue.json'; 5 | import dsGuard from './abis/DSGuard.json'; 6 | import dsChief from './abis/DSChief.json'; 7 | import dsSpell from './abis/DSSpell.json'; 8 | import dsSpellBook from './abis/DSSpellBook.json'; 9 | import dsProxy from './abis/DSProxy.json'; 10 | import dsProxyFactory from './abis/DSProxyFactory.json'; 11 | 12 | import makerOtc from './abis/MatchingMarket.json'; 13 | import saiProxy from './abis/SaiProxyCreateAndExecute.json'; 14 | import proxyRegistry from './abis/ProxyRegistry.json'; 15 | 16 | import saiTop from './abis/SaiTop.json'; 17 | import tub from './abis/SaiTub.json'; 18 | import tap from './abis/SaiTap.json'; 19 | import vox from './abis/SaiVox.json'; 20 | import mom from './abis/SaiMom.json'; 21 | import pit from './abis/GemPit.json'; 22 | 23 | const daiV1 = { 24 | saiTop, 25 | tub, 26 | tap, 27 | vox, 28 | mom, 29 | pit 30 | }; 31 | 32 | const dappHub = { 33 | dsValue, 34 | dsEthToken, 35 | dsGuard, 36 | dsChief, 37 | dsSpell, 38 | dsSpellBook, 39 | dsProxy 40 | }; 41 | 42 | const exchangesV1 = { 43 | makerOtc 44 | }; 45 | 46 | const general = { 47 | erc20 48 | }; 49 | 50 | const proxies = { 51 | saiProxy, 52 | dsProxyFactory, 53 | proxyRegistry 54 | }; 55 | 56 | export { daiV1, dappHub, exchangesV1, general, proxies }; 57 | -------------------------------------------------------------------------------- /src/core/ServiceType.js: -------------------------------------------------------------------------------- 1 | import ServiceState from './ServiceState'; 2 | 3 | const ServiceType = { 4 | LOCAL: 'LOCAL', 5 | PUBLIC: 'PUBLIC', 6 | PRIVATE: 'PRIVATE' 7 | }; 8 | 9 | const localServiceLifeCycle = { 10 | CREATED: [ServiceState.INITIALIZING], 11 | INITIALIZING: [ServiceState.CREATED, ServiceState.READY], 12 | READY: [ServiceState.ERROR], 13 | ERROR: [] 14 | }; 15 | 16 | const publicServiceLifeCycle = { 17 | CREATED: [ServiceState.INITIALIZING], 18 | INITIALIZING: [ServiceState.CREATED, ServiceState.OFFLINE], 19 | OFFLINE: [ServiceState.CONNECTING], 20 | CONNECTING: [ServiceState.OFFLINE, ServiceState.READY], 21 | READY: [ServiceState.OFFLINE, ServiceState.ERROR], 22 | ERROR: [] 23 | }; 24 | 25 | const privateServiceLifeCycle = { 26 | CREATED: [ServiceState.INITIALIZING], 27 | INITIALIZING: [ServiceState.CREATED, ServiceState.OFFLINE], 28 | OFFLINE: [ServiceState.CONNECTING], 29 | CONNECTING: [ServiceState.OFFLINE, ServiceState.ONLINE], 30 | ONLINE: [ServiceState.OFFLINE, ServiceState.AUTHENTICATING], 31 | AUTHENTICATING: [ServiceState.ONLINE, ServiceState.READY], 32 | READY: [ServiceState.OFFLINE, ServiceState.ONLINE, ServiceState.ERROR], 33 | ERROR: [] 34 | }; 35 | 36 | const serviceTypeTransitions = { 37 | LOCAL: localServiceLifeCycle, 38 | PUBLIC: publicServiceLifeCycle, 39 | PRIVATE: privateServiceLifeCycle 40 | }; 41 | 42 | export { ServiceType as default, serviceTypeTransitions }; 43 | -------------------------------------------------------------------------------- /contracts/abis/Target.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"poke","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"ouch","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import networks from '../../contracts/networks'; 2 | 3 | export function captureConsole(cb) { 4 | // eslint-disable-next-line 5 | const origConsoleLog = console.log, 6 | output = []; 7 | 8 | // eslint-disable-next-line 9 | console.log = (...args) => args.forEach(a => output.push(a)); 10 | 11 | cb(); 12 | 13 | // eslint-disable-next-line 14 | console.log = origConsoleLog; 15 | } 16 | 17 | export function promisify(fn) { 18 | return function(...args) { 19 | return new Promise((resolve, reject) => { 20 | fn.apply( 21 | this, 22 | args.concat((err, value) => (err ? reject(err) : resolve(value))) 23 | ); 24 | }); 25 | }; 26 | } 27 | 28 | export function promisifyMethods(target, methods) { 29 | return methods.reduce((output, method) => { 30 | output[method] = promisify.call(target, target[method]); 31 | return output; 32 | }, {}); 33 | } 34 | 35 | export function getNetworkName(networkId) { 36 | const result = networks.filter(n => n.networkId === networkId); 37 | 38 | if (result.length < 1) { 39 | throw new Error('No network with ID ' + networkId + ' found.'); 40 | } 41 | 42 | return result[0].name; 43 | } 44 | 45 | export function slug() { 46 | return ( 47 | '-' + 48 | Math.random() 49 | .toString(36) 50 | .substring(2, 7) + 51 | Math.random() 52 | .toString(36) 53 | .substring(2, 7) 54 | ); 55 | } 56 | 57 | export function promiseWait(ms) { 58 | return new Promise(resolve => setTimeout(resolve, ms)); 59 | } 60 | -------------------------------------------------------------------------------- /contracts/abis/DSStop.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[],"name":"stop","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"stopped","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"start","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"}] -------------------------------------------------------------------------------- /test/utils/loggers/NullLogger.spec.js: -------------------------------------------------------------------------------- 1 | import ServiceManager from '../../../src/core/ServiceManager'; 2 | import NullLogger from '../../../src/utils/loggers/NullLogger'; 3 | 4 | test('should have a valid logger interface', () => { 5 | const logger = new NullLogger(); 6 | expect(logger.trace({}, '')).toBeFalsy(); 7 | expect(logger.debug({}, '')).toBeFalsy(); 8 | expect(logger.info({}, '')).toBeFalsy(); 9 | expect(logger.warn({}, '')).toBeFalsy(); 10 | expect(logger.error({}, '')).toBeFalsy(); 11 | expect(logger.fatal({}, '')).toBeFalsy(); 12 | }); 13 | 14 | test('serviceLogger() should return a valid logger interface', () => { 15 | const logger = new NullLogger().serviceLogger( 16 | new ServiceManager('MyService').createService() 17 | ); 18 | expect(logger.trace({}, '')).toBeFalsy(); 19 | expect(logger.debug({}, '')).toBeFalsy(); 20 | expect(logger.info({}, '')).toBeFalsy(); 21 | expect(logger.warn({}, '')).toBeFalsy(); 22 | expect(logger.error({}, '')).toBeFalsy(); 23 | expect(logger.fatal({}, '')).toBeFalsy(); 24 | }); 25 | 26 | test('serviceLogger() should throw when given an invalid service object', () => { 27 | expect(() => new NullLogger().serviceLogger()).toThrow( 28 | 'Invalid service object' 29 | ); 30 | expect(() => new NullLogger().serviceLogger({})).toThrow( 31 | 'Invalid service object' 32 | ); 33 | expect(() => new NullLogger().serviceLogger({ manager: null })).toThrow( 34 | 'Invalid service object' 35 | ); 36 | expect(() => new NullLogger().serviceLogger({ manager: () => ({}) })).toThrow( 37 | 'Invalid service object' 38 | ); 39 | }); 40 | -------------------------------------------------------------------------------- /contracts/abis/ERC20.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"guy","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"guy","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"}] -------------------------------------------------------------------------------- /test/helpers/serviceBuilders.js: -------------------------------------------------------------------------------- 1 | import DefaultServiceProvider from '../../src/config/DefaultServiceProvider'; 2 | import ProviderType from '../../src/eth/web3/ProviderType'; 3 | 4 | export const kovanProviderConfig = { 5 | web3: { 6 | privateKey: process.env.KOVAN_PRIVATE_KEY, 7 | provider: { 8 | type: ProviderType.INFURA, 9 | network: 'kovan', 10 | infuraApiKey: process.env.INFURA_API_KEY 11 | } 12 | } 13 | }; 14 | 15 | export const defaultProviderConfig = { 16 | web3: { 17 | provider: { type: ProviderType.TEST }, 18 | transactionSettings: { 19 | gasLimit: 4000000 20 | } 21 | }, 22 | log: false 23 | }; 24 | 25 | const cache = { storage: {} }; 26 | 27 | export function resetCache() { 28 | cache.storage = {}; 29 | } 30 | 31 | export function buildTestContainer(settings) { 32 | return new DefaultServiceProvider({ 33 | ...defaultProviderConfig, 34 | // ...kovanProviderConfig, 35 | // cache, 36 | ...settings 37 | }); 38 | } 39 | 40 | export function buildTestService(name, settings) { 41 | return buildTestContainer(settings).service(name); 42 | } 43 | 44 | export function buildTestEthereumCdpService() { 45 | return buildTestService('cdp', { cdp: true }); 46 | } 47 | 48 | export function buildTestEthereumTokenService() { 49 | return buildTestService('token', { token: true }); 50 | } 51 | 52 | export function buildTestSmartContractService() { 53 | return buildTestService('smartContract', { smartContract: true }); 54 | } 55 | 56 | export function buildTestEventService() { 57 | return buildTestService('event', { event: true }); 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/events/helpers.js: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash.isequal'; 2 | 3 | ////////////////////////////// 4 | ///// Polling Helpers ////// 5 | ////////////////////////////// 6 | 7 | export function createPayloadFetcher(payloadGetterMap) { 8 | return () => { 9 | return Promise.all( 10 | Object.entries(payloadGetterMap).map(([key, getter]) => 11 | getter().then(state => [key, state]) 12 | ) 13 | ).then(states => { 14 | const payload = {}; 15 | for (const [key, state] of states) { 16 | payload[key] = state; 17 | } 18 | return payload; 19 | }); 20 | }; 21 | } 22 | 23 | export function createMemoizedPoll({ 24 | type, 25 | getState, 26 | emit, 27 | curr = {}, 28 | live = false 29 | }) { 30 | return { 31 | async ping() { 32 | if (!live) return; 33 | try { 34 | const next = await getState(); 35 | if (!isEqual(curr, next)) { 36 | emit(type, next); 37 | curr = next; 38 | } 39 | } catch (err) { 40 | const msg = `Failed to get latest ${type} state. Message -> ${err}`; 41 | emit('error', msg); 42 | } 43 | }, 44 | async heat() { 45 | if (live) return; 46 | try { 47 | curr = await getState(); 48 | live = true; 49 | } catch (err) { 50 | const msg = `Failed to get initial ${type} state. Message -> ${err}`; 51 | emit('error', msg); 52 | } 53 | }, 54 | cool() { 55 | if (!live) return; 56 | live = false; 57 | }, 58 | type() { 59 | return type; 60 | }, 61 | live() { 62 | return live; 63 | } 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /contracts/abis/DSTokenBase.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"src","type":"address"},{"name":"guy","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"supply","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"}] -------------------------------------------------------------------------------- /src/eth/tokens/Erc20Token.js: -------------------------------------------------------------------------------- 1 | import { currencies, getCurrency } from '../Currency'; 2 | 3 | export default class Erc20Token { 4 | constructor(contract, web3Service, decimals = 18, symbol) { 5 | this._contract = contract; 6 | this._web3Service = web3Service; 7 | this._decimals = decimals; 8 | this.symbol = symbol; 9 | this._currency = currencies[symbol]; 10 | } 11 | 12 | async allowance(tokenOwner, spender) { 13 | return this._valueFromContract( 14 | await this._contract.allowance(tokenOwner, spender) 15 | ); 16 | } 17 | 18 | async balanceOf(owner) { 19 | return this._valueFromContract(await this._contract.balanceOf(owner)); 20 | } 21 | 22 | async totalSupply() { 23 | return this._valueFromContract(await this._contract.totalSupply()); 24 | } 25 | 26 | address() { 27 | return this._contract.address; 28 | } 29 | 30 | _valueForContract(value, unit = this._currency) { 31 | return getCurrency(value, unit).toEthersBigNumber(this._decimals); 32 | } 33 | 34 | _valueFromContract(value) { 35 | return this._currency(value, -1 * this._decimals); 36 | } 37 | 38 | approve(spender, value, unit = this._currency) { 39 | return this._contract.approve(spender, this._valueForContract(value, unit)); 40 | } 41 | 42 | approveUnlimited(spender) { 43 | return this._contract.approve(spender, -1); 44 | } 45 | 46 | transfer(to, value, unit = currencies[this.symbol]) { 47 | return this._contract.transfer(to, this._valueForContract(value, unit)); 48 | } 49 | 50 | transferFrom(from, to, value, unit = currencies[this.symbol]) { 51 | return this._contract.transferFrom( 52 | from, 53 | to, 54 | this._valueForContract(value, unit) 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/eth/AllowanceService.js: -------------------------------------------------------------------------------- 1 | import PrivateService from '../core/PrivateService'; 2 | import BigNumber from 'bignumber.js'; 3 | import { UINT256_MAX } from '../utils/constants'; 4 | 5 | const maxAllowance = BigNumber(UINT256_MAX).shiftedBy(-18); 6 | 7 | export default class AllowanceService extends PrivateService { 8 | constructor(name = 'allowance') { 9 | super(name, ['token']); 10 | this._shouldMinimizeAllowance = false; 11 | } 12 | 13 | initialize(settings) { 14 | if (settings && settings.useMinimizeAllowancePolicy) { 15 | this._shouldMinimizeAllowance = true; 16 | } 17 | } 18 | 19 | async requireAllowance( 20 | tokenSymbol, 21 | spenderAddress, 22 | amountEstimate = maxAllowance 23 | ) { 24 | const token = this.get('token').getToken(tokenSymbol); 25 | const ownerAddress = this.get('token') 26 | .get('web3') 27 | .currentAccount(); 28 | const allowance = await token.allowance(ownerAddress, spenderAddress); 29 | 30 | if (allowance.lt(maxAllowance.div(2)) && !this._shouldMinimizeAllowance) { 31 | return token.approveUnlimited(spenderAddress); 32 | } 33 | 34 | if (allowance.lt(amountEstimate) && this._shouldMinimizeAllowance) { 35 | return token.approve(spenderAddress, amountEstimate); 36 | } 37 | } 38 | 39 | removeAllowance(tokenSymbol, spenderAddress) { 40 | const token = this.get('token').getToken(tokenSymbol); 41 | return token 42 | .allowance( 43 | this.get('token') 44 | .get('web3') 45 | .currentAccount(), 46 | spenderAddress 47 | ) 48 | .then(allowance => { 49 | if (parseInt(allowance) != 0) { 50 | return token.approve(spenderAddress, '0'); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /contracts/abis/DSValue.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wut","type":"bytes32"}],"name":"poke","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"read","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"peek","outputs":[{"name":"","type":"bytes32"},{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"void","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /src/eth/GasEstimatorService.js: -------------------------------------------------------------------------------- 1 | import PublicService from '../core/PublicService'; 2 | 3 | export default class GasEstimatorService extends PublicService { 4 | constructor(name = 'gasEstimator') { 5 | super(name, ['web3', 'log']); 6 | this._percentage = null; 7 | this._absolute = null; 8 | } 9 | 10 | estimateGasLimit(transaction) { 11 | if (this._percentage === null && this._absolute === null) { 12 | throw new Error('no gas limit policy set'); 13 | } 14 | 15 | return Promise.all([ 16 | this.get('web3').eth.getBlock('latest'), 17 | this.get('web3').eth.estimateGas(transaction) 18 | ]).then(web3Data => { 19 | const blockLimit = web3Data[0].gasLimit, 20 | estimate = web3Data[1]; 21 | 22 | if (this._percentage === null && this._absolute !== null) { 23 | return Math.min(this._absolute, blockLimit); 24 | } 25 | 26 | if (this._absolute === null) { 27 | return Math.min(estimate * this._percentage, blockLimit); 28 | } 29 | 30 | return Math.min(estimate * this._percentage, this._absolute, blockLimit); 31 | }); 32 | } 33 | 34 | setPercentage(number) { 35 | if (number <= 0) { 36 | throw new Error('gas limit percentage must be greater than 0'); 37 | } 38 | this._percentage = number; 39 | } 40 | 41 | setAbsolute(number) { 42 | if (number <= 0) { 43 | throw new Error('gas limit must be greater than 0'); 44 | } 45 | 46 | this._absolute = number; 47 | } 48 | 49 | removePercentage() { 50 | this._percentage = null; 51 | } 52 | 53 | removeAbsolute() { 54 | this._absolute = null; 55 | } 56 | 57 | getPercentage() { 58 | return this._percentage; 59 | } 60 | 61 | getAbsolute() { 62 | return this._absolute; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/eth/NonceService.js: -------------------------------------------------------------------------------- 1 | import PublicService from '../core/PublicService'; 2 | import { promisify } from '../utils'; 3 | 4 | export default class NonceService extends PublicService { 5 | constructor(name = 'nonce') { 6 | super(name, ['web3', 'accounts']); 7 | this._counts = {}; 8 | } 9 | 10 | async connect() { 11 | this._accountsService = this.get('accounts'); 12 | this._web3Service = this.get('web3'); 13 | await this.setCounts(); 14 | } 15 | 16 | async _getTxCount(address) { 17 | return promisify(this._web3Service._web3.eth.getTransactionCount)( 18 | address, 19 | 'pending' 20 | ); 21 | } 22 | 23 | _compareNonceCounts(txCount, address) { 24 | if (txCount > this._counts[address]) { 25 | return txCount; 26 | } else { 27 | return this._counts[address]; 28 | } 29 | } 30 | 31 | async setCounts() { 32 | const accountsList = await this._accountsService.listAccounts(); 33 | 34 | return new Promise(resolve => { 35 | accountsList.map(async account => { 36 | const txCount = await this._getTxCount(account.address); 37 | this._counts[account.address] = txCount; 38 | 39 | if (Object.keys(this._counts).length === accountsList.length) { 40 | resolve(); 41 | } 42 | }); 43 | }); 44 | } 45 | 46 | async getNonce() { 47 | const address = this._web3Service.currentAccount(); 48 | const txCount = await this._getTxCount(address); 49 | let nonce; 50 | 51 | if (this._counts[address]) { 52 | nonce = this._compareNonceCounts(txCount, address); 53 | this._counts[address] += 1; 54 | } else { 55 | this._counts[address] = txCount; 56 | nonce = txCount; 57 | this._counts[address] += 1; 58 | } 59 | 60 | return nonce; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/helpers/TestAccountProvider.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | default as provider, 3 | TestAccountProvider 4 | } from '../helpers/TestAccountProvider'; 5 | 6 | test('should reject illegal indices', () => { 7 | const msg = 8 | 'Index must be a natural number between 0 and ' + 9 | (provider._accounts.addresses.length - 1); 10 | expect(() => provider.setIndex(-1)).toThrow(msg); 11 | expect(() => provider.setIndex(provider._accounts.length)).toThrow(msg); 12 | expect(() => provider.setIndex('Not a number')).toThrow(msg); 13 | }); 14 | 15 | test('should reject accounts objects', () => { 16 | expect( 17 | () => new TestAccountProvider({ addresses: ['x'], keeys: ['y'] }) 18 | ).toThrow('Accounts must be an object with properties addresses and keys'); 19 | 20 | expect( 21 | () => 22 | new TestAccountProvider({ addresses: ['x', 'y', 'z'], keys: ['x', 'y'] }) 23 | ).toThrow('Accounts addresses and keys arrays must have the same length'); 24 | }); 25 | 26 | test('test provider provides 1000 addresses and keys', () => { 27 | const originalIndex = provider.getIndex(); 28 | let error = -1; 29 | 30 | provider.setIndex(0); 31 | for (let i = 0; i < 500; i++) { 32 | let account = provider.nextAccount(); 33 | if ( 34 | typeof account.address !== 'string' || 35 | typeof account.key !== 'string' 36 | ) { 37 | error = i; 38 | } 39 | } 40 | 41 | for (let i = 0; i < 500; i++) { 42 | let address = provider.nextAddress(); 43 | if (typeof address !== 'string') { 44 | error = i; 45 | } 46 | } 47 | 48 | expect(error).toBe(-1); 49 | expect(() => provider.nextAccount()).toThrow( 50 | 'No more test accounts available.' 51 | ); 52 | 53 | provider.setIndex(originalIndex); 54 | expect(provider.getIndex()).toBe(originalIndex); 55 | }); 56 | -------------------------------------------------------------------------------- /src/eth/accounts/factories.js: -------------------------------------------------------------------------------- 1 | import ethUtil from 'ethereumjs-util'; 2 | import Wallet from 'web3-provider-engine/dist/es5/subproviders/wallet'; 3 | import { getBrowserProvider } from './setup'; 4 | 5 | export function privateKeyAccountFactory({ key }) { 6 | if (typeof key != 'string' || !key.match(/^(0x)?[0-9a-fA-F]{64}$/)) { 7 | throw new Error('Invalid private key format'); 8 | } 9 | 10 | const [keyWithPrefix, keySansPrefix] = key.startsWith('0x') 11 | ? [key, key.replace(/^0x/, '')] 12 | : ['0x' + key, key]; 13 | 14 | const address = 15 | '0x' + ethUtil.privateToAddress(keyWithPrefix).toString('hex'); 16 | const keyBuffer = Buffer.from(keySansPrefix, 'hex'); 17 | 18 | const subprovider = new Wallet( 19 | { getAddressString: () => address, getPrivateKey: () => keyBuffer }, 20 | {} 21 | ); 22 | 23 | return { subprovider, address }; 24 | } 25 | 26 | async function getAccountAddress(subprovider) { 27 | return new Promise((resolve, reject) => 28 | subprovider.handleRequest( 29 | { method: 'eth_accounts', params: [], id: 1 }, 30 | null, 31 | (err, val) => (err ? reject(err) : resolve(val[0])) 32 | ) 33 | ); 34 | } 35 | 36 | export async function providerAccountFactory(_, provider) { 37 | // we need to be able to swap out this account while leaving the original 38 | // provider in place for other accounts, so the subprovider here has to be 39 | // a different instance. using Proxy is a simple way to accomplish this. 40 | const subprovider = new Proxy(provider, {}); 41 | return { subprovider, address: await getAccountAddress(subprovider) }; 42 | } 43 | 44 | export async function browserProviderAccountFactory() { 45 | const subprovider = await getBrowserProvider(); 46 | return { subprovider, address: await getAccountAddress(subprovider) }; 47 | } 48 | -------------------------------------------------------------------------------- /scripts/showServiceDependencies.js: -------------------------------------------------------------------------------- 1 | // run this with babel-node: 2 | // > npx babel-node scripts/showServiceDependencies.js 3 | 4 | import DefaultServiceProvider, { resolver } from '../src/config/DefaultServiceProvider'; 5 | import chalk from 'chalk'; 6 | import times from 'lodash.times'; 7 | 8 | 9 | const colors = [ 10 | '#e6194b', 11 | '#3cb44b', 12 | '#ffe119', 13 | '#0082c8', 14 | '#f58231', 15 | '#911eb4', 16 | '#46f0f0', 17 | '#f032e6', 18 | '#d2f53c', 19 | '#fabebe', 20 | '#008080', 21 | '#e6beff', 22 | '#aa6e28', 23 | '#fffac8', 24 | '#800000', 25 | '#aaffc3', 26 | '#808000', 27 | '#ffd8b1', 28 | '#000080', 29 | '#808080' 30 | ]; 31 | 32 | const colorMap = {}; 33 | let colorMapCounter = 0; 34 | 35 | const colorize = function(serviceName) { 36 | if (!colorMap[serviceName]) { 37 | if (colorMapCounter >= colors.length) { 38 | return chalk.reset(serviceName); 39 | } 40 | colorMap[serviceName] = colors[colorMapCounter]; 41 | colorMapCounter++; 42 | } 43 | 44 | return chalk.hex(colorMap[serviceName]).bold(serviceName); 45 | }; 46 | 47 | const NAME_MAX_LENGTH = 18; 48 | 49 | function colorizeAndPad(name) { 50 | return [colorize(name)] 51 | .concat(times(NAME_MAX_LENGTH - name.length, () => ' ')) 52 | .join(''); 53 | } 54 | 55 | const config = Object.keys(resolver.defaults).reduce((acc, key) => { 56 | acc[key] = true; 57 | return acc; 58 | }, {}); 59 | const container = new DefaultServiceProvider(config).buildContainer(); 60 | for (let key of Object.keys(container._services).sort()) { 61 | const service = container._services[key]; 62 | const name = service.manager().name(); 63 | const depNames = service.manager().dependencies(); 64 | console.log( 65 | `${colorizeAndPad(name)} : ${depNames.sort().map(colorize).join(', ') || 66 | 'none'}` 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/eth/smartContract/wrapContract.js: -------------------------------------------------------------------------------- 1 | export function wrapContract(contract, name, abi, txManager, businessObject) { 2 | const nonConstantFns = {}; 3 | for (let { type, constant, name, inputs } of abi) { 4 | if (type === 'function' && constant === false) { 5 | // Map all of the contract method names + sigs in cases where the method sig is used 6 | // as the key to call the func. e.g. contract["method(address,uint256)"](foo, bar) 7 | // for when there are multiple overloaded methods 8 | if (inputs.length > 0) { 9 | const methodSig = `${name}(${inputs.map(i => i.type).join(',')})`; 10 | nonConstantFns[methodSig] = true; 11 | } 12 | // Currently assume that the default method chosen by Ethers when there 13 | // are multiple overloaded methods of the same name is non-constant 14 | nonConstantFns[name] = true; 15 | } 16 | } 17 | // The functions in ethers.Contract are set up as read-only, non-configurable 18 | // properties, which means if we try to change their values with Proxy, we 19 | // get an error. See https://stackoverflow.com/a/48495509/56817 for more 20 | // detail. 21 | // 22 | // But that only happens if the contract is specified as the first argument 23 | // to Proxy. So we don't do that. Go on, wag your finger. 24 | const proxy = new Proxy( 25 | {}, 26 | { 27 | get(target, key) { 28 | if (nonConstantFns[key] && txManager) { 29 | return (...args) => { 30 | return txManager.formatHybridTx( 31 | contract, 32 | key, 33 | args, 34 | name, 35 | businessObject 36 | ); 37 | }; 38 | } 39 | 40 | return contract[key]; 41 | }, 42 | 43 | set(target, key, value) { 44 | contract[key] = value; 45 | return true; 46 | } 47 | } 48 | ); 49 | 50 | return proxy; 51 | } 52 | -------------------------------------------------------------------------------- /test/eth/EthereumTokenService.spec.js: -------------------------------------------------------------------------------- 1 | import { buildTestEthereumTokenService } from '../helpers/serviceBuilders'; 2 | import tokens from '../../contracts/tokens'; 3 | import { MKR } from '../../src/eth/Currency'; 4 | 5 | let ethereumTokenService; 6 | 7 | beforeAll(async () => { 8 | ethereumTokenService = buildTestEthereumTokenService(); 9 | await ethereumTokenService.manager().authenticate(); 10 | }); 11 | 12 | test('getTokens returns tokens', () => { 13 | const tokensList = ethereumTokenService.getTokens(); 14 | expect(tokensList.includes(tokens.DAI)).toBe(true); 15 | expect(tokensList.includes(tokens.MKR)).toBe(true); 16 | }); 17 | 18 | test('getTokenVersions returns token versions using remote blockchain', () => { 19 | const tokenVersions = ethereumTokenService.getTokenVersions(); 20 | 21 | expect(tokenVersions[tokens.MKR]).toEqual([1, 2]); 22 | expect(tokenVersions[tokens.DAI]).toEqual([1]); 23 | expect(tokenVersions[tokens.ETH]).toEqual([1]); 24 | 25 | expect( 26 | ethereumTokenService.getToken(tokens.MKR)._contract.address.toUpperCase() 27 | ).toBe( 28 | ethereumTokenService.getToken(tokens.MKR, 2)._contract.address.toUpperCase() 29 | ); 30 | }); 31 | 32 | test('getToken returns token object of correct version', () => { 33 | expect( 34 | ethereumTokenService.getToken(tokens.MKR)._contract.address.toUpperCase() 35 | ).toBe( 36 | ethereumTokenService.getToken(tokens.MKR, 2)._contract.address.toUpperCase() 37 | ); 38 | 39 | expect( 40 | ethereumTokenService.getToken(tokens.MKR)._contract.address.toUpperCase() 41 | ).not.toBe( 42 | ethereumTokenService.getToken(tokens.MKR, 1)._contract.address.toUpperCase() 43 | ); 44 | }); 45 | 46 | test('getToken throws when given unknown token symbol', () => { 47 | expect(() => ethereumTokenService.getToken('XYZ')).toThrow(); 48 | }); 49 | 50 | test('getToken works with Currency', () => { 51 | const token = ethereumTokenService.getToken(MKR); 52 | expect(token.symbol).toBe('MKR'); 53 | }); 54 | -------------------------------------------------------------------------------- /test/eth/EthereumCdpService.spec.js: -------------------------------------------------------------------------------- 1 | import { buildTestEthereumCdpService } from '../helpers/serviceBuilders'; 2 | import { USD_DAI } from '../../src/eth/Currency'; 3 | import Cdp from '../../src/eth/Cdp'; 4 | 5 | let cdpService; 6 | 7 | beforeAll(async () => { 8 | cdpService = buildTestEthereumCdpService(); 9 | await cdpService.manager().authenticate(); 10 | }); 11 | 12 | afterAll(async () => { 13 | // other tests expect this to be the case 14 | await cdpService.get('price').setEthPrice('400'); 15 | }); 16 | 17 | test('can read the liquidation ratio', async () => { 18 | const liquidationRatio = await cdpService.getLiquidationRatio(); 19 | expect(liquidationRatio.toString()).toEqual('1.5'); 20 | }); 21 | 22 | test('can read the liquidation penalty', async () => { 23 | const liquidationPenalty = await cdpService.getLiquidationPenalty(); 24 | expect(liquidationPenalty.toString()).toEqual('0.13'); 25 | }); 26 | 27 | test('can read the annual governance fee', async () => { 28 | const governanceFee = await cdpService.getAnnualGovernanceFee(); 29 | expect(governanceFee.toFixed(3)).toEqual('0.005'); 30 | }); 31 | 32 | test('can read the target price', async () => { 33 | const tp = await cdpService.getTargetPrice(); 34 | expect(tp).toEqual(USD_DAI(1)); 35 | }); 36 | 37 | test('can calculate system collateralization', async () => { 38 | const cdp = await cdpService.openCdp(); 39 | await cdp.lockEth(0.1); 40 | await cdp.drawDai(1); 41 | const scA = await cdpService.getSystemCollateralization(); 42 | 43 | await cdp.lockEth(0.1); 44 | const scB = await cdpService.getSystemCollateralization(); 45 | expect(scB).toBeGreaterThan(scA); 46 | 47 | await cdp.drawDai(1); 48 | const scC = await cdpService.getSystemCollateralization(); 49 | expect(scC).toBeLessThan(scB); 50 | }); 51 | 52 | test('openCdp returns the transaction object', async () => { 53 | const txo = cdpService.openCdp(); 54 | expect(txo).toBeInstanceOf(Promise); 55 | const cdp = await txo; 56 | expect(cdp).toBeInstanceOf(Cdp); 57 | }); 58 | -------------------------------------------------------------------------------- /src/eth/tokens/EtherToken.js: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers'; 2 | import { getCurrency, ETH } from '../Currency'; 3 | 4 | export default class EtherToken { 5 | constructor(web3Service, gasEstimatorService, transactionManager) { 6 | this._web3 = web3Service; 7 | this._gasEstimator = gasEstimatorService; 8 | this._transactionManager = transactionManager; 9 | } 10 | 11 | // eslint-disable-next-line 12 | allowance(tokenOwner, spender) { 13 | return Promise.resolve(Number.MAX_SAFE_INTEGER); 14 | } 15 | 16 | balanceOf(owner) { 17 | return this._web3 18 | .ethersProvider() 19 | .getBalance(owner) 20 | .then(b => utils.formatEther(b)); 21 | } 22 | 23 | // eslint-disable-next-line 24 | approve(spender, value) { 25 | return Promise.resolve(true); 26 | } 27 | 28 | // eslint-disable-next-line 29 | approveUnlimited(spender) { 30 | return Promise.resolve(true); 31 | } 32 | 33 | async transfer(toAddress, amount, unit = ETH) { 34 | const value = getCurrency(amount, unit) 35 | .toEthersBigNumber('wei') 36 | .toString(); 37 | const nonce = await this._transactionManager.get('nonce').getNonce(); 38 | const currentAccount = this._web3.currentAccount(); 39 | const tx = this._web3.eth.sendTransaction({ 40 | from: currentAccount, 41 | to: toAddress, 42 | value, 43 | nonce: nonce 44 | //gasPrice: 500000000 45 | }); 46 | 47 | return this._transactionManager.createHybridTx( 48 | tx.then(tx => ({ hash: tx })) 49 | ); 50 | } 51 | 52 | async transferFrom(fromAddress, toAddress, transferValue) { 53 | const nonce = await this._transactionManager.get('nonce').getNonce(); 54 | const valueInWei = utils.parseEther(transferValue).toString(); 55 | const tx = this._web3.eth.sendTransaction({ 56 | nonce: nonce, 57 | from: fromAddress, 58 | to: toAddress, 59 | value: valueInWei 60 | }); 61 | 62 | return this._transactionManager.createHybridTx( 63 | tx.then(tx => ({ hash: tx })) 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/eth/NonceService.spec.js: -------------------------------------------------------------------------------- 1 | import { buildTestService } from '../helpers/serviceBuilders'; 2 | 3 | let nonceService; 4 | 5 | beforeEach(async () => { 6 | nonceService = buildTestService('nonce', { nonce: true }); 7 | await nonceService.manager().authenticate(); 8 | }); 9 | 10 | function currentAccount() { 11 | return nonceService._web3Service.currentAccount(); 12 | } 13 | 14 | test('should properly fetch the transaction count', async () => { 15 | const count = await nonceService._getTxCount(currentAccount()); 16 | 17 | expect(typeof count).toEqual('number'); 18 | expect(count).toBeGreaterThan(0); 19 | }); 20 | 21 | test('should properly initialize the counts in state', async () => { 22 | const originalCounts = nonceService._counts; 23 | nonceService._counts = {}; 24 | await nonceService.setCounts(); 25 | 26 | expect(typeof nonceService._counts).toEqual('object'); 27 | expect(nonceService._counts).toEqual(originalCounts); 28 | expect(Object.keys(nonceService._counts).includes(currentAccount())); 29 | }); 30 | 31 | test('should set different counts for each signer', async () => { 32 | const accountsList = nonceService._accountsService.listAccounts(); 33 | await nonceService.setCounts(); 34 | 35 | expect(typeof nonceService._counts).toEqual('object'); 36 | expect(Object.keys(nonceService._counts).length).toEqual(accountsList.length); 37 | }); 38 | 39 | test('should return its own tx count if higher than count from node', async () => { 40 | nonceService._counts[currentAccount()] = 500000; 41 | const nonce = await nonceService.getNonce(); 42 | 43 | expect(nonce).toEqual(500000); 44 | }); 45 | 46 | test('should return tx count from node if higher than own count', async () => { 47 | nonceService._counts[currentAccount()] = 0; 48 | const nonce = await nonceService.getNonce(); 49 | 50 | expect(nonce).not.toEqual(0); 51 | }); 52 | 53 | test('should return a nonce even when own count is undefined', async () => { 54 | nonceService._counts[currentAccount()] = undefined; 55 | const nonce = await nonceService.getNonce(); 56 | 57 | expect(typeof nonce).toEqual('number'); 58 | }); 59 | -------------------------------------------------------------------------------- /src/eth/PriceService.js: -------------------------------------------------------------------------------- 1 | import PrivateService from '../core/PrivateService'; 2 | import contracts from '../../contracts/contracts'; 3 | import { RAY } from '../utils/constants'; 4 | import BigNumber from 'bignumber.js'; 5 | import { utils } from 'ethers'; 6 | import { getCurrency, ETH, USD_PETH, MKR, USD_ETH, USD_MKR } from './Currency'; 7 | 8 | export default class PriceService extends PrivateService { 9 | /** 10 | * @param {string} name 11 | */ 12 | 13 | constructor(name = 'price') { 14 | super(name, ['token', 'smartContract', 'event']); 15 | } 16 | 17 | initialize() { 18 | this.get('event').registerPollEvents({ 19 | 'price/ETH_USD': { 20 | price: () => this.getEthPrice() 21 | }, 22 | 'price/MKR_USD': { 23 | price: () => this.getMkrPrice() 24 | }, 25 | 'price/WETH_PETH': { 26 | ratio: () => this.getWethToPethRatio() 27 | } 28 | }); 29 | } 30 | 31 | _getContract(contract) { 32 | return this.get('smartContract').getContractByName(contract); 33 | } 34 | 35 | _valueForContract(value, unit) { 36 | const bn = getCurrency(value, unit).toEthersBigNumber('wei'); 37 | return utils.hexlify(utils.padZeros(bn, 32)); 38 | } 39 | 40 | getWethToPethRatio() { 41 | return this._getContract(contracts.SAI_TUB) 42 | .per() 43 | .then(bn => new BigNumber(bn.toString()).dividedBy(RAY).toNumber()); 44 | } 45 | 46 | async getEthPrice() { 47 | return USD_ETH.wei(await this._getContract(contracts.SAI_PIP).read()); 48 | } 49 | 50 | async getPethPrice() { 51 | return USD_PETH.ray(await this._getContract(contracts.SAI_TUB).tag()); 52 | } 53 | 54 | async getMkrPrice() { 55 | return USD_MKR.wei((await this._getContract(contracts.SAI_PEP).peek())[0]); 56 | } 57 | 58 | setEthPrice(newPrice, unit = ETH) { 59 | const value = this._valueForContract(newPrice, unit); 60 | return this._getContract(contracts.SAI_PIP).poke(value); 61 | } 62 | 63 | setMkrPrice(newPrice, unit = MKR) { 64 | const value = this._valueForContract(newPrice, unit); 65 | return this._getContract(contracts.SAI_PEP).poke(value); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/helpers/TestAccountProvider.js: -------------------------------------------------------------------------------- 1 | import accounts from './testAccounts'; 2 | 3 | /** 4 | * 5 | */ 6 | class TestAccountProvider { 7 | constructor(accounts, initialIndex = 0) { 8 | this._setAccounts(accounts); 9 | this.setIndex(initialIndex); 10 | } 11 | 12 | nextAddress() { 13 | return this._next().address; 14 | } 15 | 16 | nextAccount() { 17 | return this._next(); 18 | } 19 | 20 | getIndex() { 21 | return this._index; 22 | } 23 | 24 | setIndex(i) { 25 | if ( 26 | typeof i !== 'number' || 27 | i < 0 || 28 | i >= this._accounts.addresses.length 29 | ) { 30 | throw new Error( 31 | 'Index must be a natural number between 0 and ' + 32 | (this._accounts.addresses.length - 1) + 33 | ', got: ' + 34 | i 35 | ); 36 | } 37 | this._setIndex(i); 38 | } 39 | 40 | _setIndex(i) { 41 | this._index = i; 42 | // eslint-disable-next-line 43 | process.env._TestProviderIndex = '' + this._index; 44 | } 45 | 46 | _setAccounts(accounts) { 47 | if (typeof accounts !== 'object' || !accounts.addresses || !accounts.keys) { 48 | throw new Error( 49 | 'Accounts must be an object with properties addresses and keys' 50 | ); 51 | } 52 | 53 | if (accounts.addresses.length !== accounts.keys.length) { 54 | throw new Error( 55 | 'Accounts addresses and keys arrays must have the same length' 56 | ); 57 | } 58 | 59 | this._accounts = accounts; 60 | } 61 | 62 | _next() { 63 | if (this._index >= this._accounts.addresses.length) { 64 | throw new Error('No more test accounts available.'); 65 | } 66 | const i = this.getIndex(); 67 | this._setIndex(i + 1); 68 | 69 | return { 70 | address: this._accounts.addresses[i], 71 | key: '0x' + this._accounts.keys[i] 72 | }; 73 | } 74 | } 75 | // eslint-disable-next-line 76 | const i = process.env._TestProviderIndex 77 | ? // eslint-disable-next-line 78 | parseInt(process.env._TestProviderIndex) 79 | : 1; 80 | const p = new TestAccountProvider(accounts, i); 81 | export { p as default, TestAccountProvider }; 82 | -------------------------------------------------------------------------------- /test/Maker.spec.js: -------------------------------------------------------------------------------- 1 | import Maker, { ETH, LocalService } from '../src/index'; 2 | import TestAccountProvider from './helpers/TestAccountProvider'; 3 | const Maker2 = require('../src/index'); 4 | 5 | function createMaker(privateKey) { 6 | return Maker.create('test', { privateKey, log: false }); 7 | } 8 | 9 | test('import vs require', () => { 10 | expect(Maker2).toEqual(Maker); 11 | expect(Maker2.ETH).toEqual(ETH); 12 | expect(Maker2.LocalService).toEqual(LocalService); 13 | }); 14 | 15 | test('openCdp', async () => { 16 | const maker = createMaker(); 17 | await maker.authenticate(); 18 | const cdp = await maker.openCdp(); 19 | const id = await cdp.getId(); 20 | expect(typeof id).toBe('number'); 21 | expect(id).toBeGreaterThan(0); 22 | }); 23 | 24 | test( 25 | 'openCdp with a private key', 26 | async () => { 27 | const testAccount = TestAccountProvider.nextAccount(); 28 | 29 | const maker = createMaker(testAccount.key); 30 | await maker.authenticate(); 31 | const cdp = await maker.openCdp(); 32 | const id = await cdp.getId(); 33 | expect(typeof id).toBe('number'); 34 | expect(id).toBeGreaterThan(0); 35 | const info = await cdp.getInfo(); 36 | expect(info.lad.toLowerCase()).toEqual(testAccount.address); 37 | }, 38 | 5000 39 | ); 40 | 41 | test('creates a new CDP object for existing CDPs', async () => { 42 | const maker = createMaker(); 43 | await maker.authenticate(); 44 | const cdp = await maker.openCdp(); 45 | const id = await cdp.getId(); 46 | const newCdp = await maker.getCdp(id); 47 | expect(id).toEqual(await newCdp.getId()); 48 | expect(cdp._cdpService).toEqual(newCdp._cdpService); 49 | expect(cdp._smartContractService).toEqual(newCdp._smartContractService); 50 | }); 51 | 52 | test('throws an error for an invalid id', async () => { 53 | const maker = createMaker(); 54 | await maker.authenticate(); 55 | expect.assertions(1); 56 | try { 57 | await maker.getCdp(99999); 58 | } catch (err) { 59 | expect(err.message).toMatch(/CDP doesn't exist/); 60 | } 61 | }); 62 | 63 | test('exports currency types', () => { 64 | expect(Maker.ETH(1).toString()).toEqual('1.00 ETH'); 65 | }); 66 | -------------------------------------------------------------------------------- /contracts/abis/DSProxy.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_target","type":"address"},{"name":"_data","type":"bytes"}],"name":"execute","outputs":[{"name":"response","type":"bytes32"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes"},{"name":"_data","type":"bytes"}],"name":"execute","outputs":[{"name":"target","type":"address"},{"name":"response","type":"bytes32"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"cache","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_cacheAddr","type":"address"}],"name":"setCache","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_cacheAddr","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /test/eth/tokens/PethToken.spec.js: -------------------------------------------------------------------------------- 1 | import { buildTestEthereumTokenService } from '../../helpers/serviceBuilders'; 2 | import contracts from '../../../contracts/contracts'; 3 | import TestAccountProvider from '../../helpers/TestAccountProvider'; 4 | import { WETH, PETH } from '../../../src/eth/Currency'; 5 | 6 | let tokenService, owner, weth, peth; 7 | 8 | beforeAll(async () => { 9 | tokenService = buildTestEthereumTokenService(); 10 | await tokenService.manager().authenticate(); 11 | owner = tokenService.get('web3').currentAccount(); 12 | weth = tokenService.getToken(WETH); 13 | peth = tokenService.getToken(PETH); 14 | }); 15 | 16 | test('get PETH balance of address', async () => { 17 | const balance = await peth.balanceOf(TestAccountProvider.nextAddress()); 18 | expect(balance).toEqual(PETH(0)); 19 | }); 20 | 21 | test('get PETH allowance of address', async () => { 22 | const allowance = await peth.allowance( 23 | TestAccountProvider.nextAddress(), 24 | TestAccountProvider.nextAddress() 25 | ); 26 | expect(allowance).toEqual(PETH(0)); 27 | }); 28 | 29 | test('should successfully join and exit PETH', async () => { 30 | const tub = tokenService 31 | .get('smartContract') 32 | .getContractByName(contracts.SAI_TUB); 33 | await weth.approveUnlimited(tub.address); 34 | await peth.approveUnlimited(tub.address); 35 | 36 | await weth.deposit(0.1); 37 | const balance1 = await peth.balanceOf(owner); 38 | 39 | await peth.join(0.1); 40 | const balance2 = await peth.balanceOf(owner); 41 | expect(balance1.plus(0.1)).toEqual(balance2); 42 | 43 | await peth.exit(0.1); 44 | const balance3 = await peth.balanceOf(owner); 45 | expect(balance2.minus(0.1)).toEqual(balance3); 46 | }); 47 | 48 | test('should return the wrapper ratio', async () => { 49 | const ratio = await peth.wrapperRatio(); 50 | expect(ratio).toEqual(WETH(1)); 51 | }); 52 | 53 | test('should return the join price in weth', async () => { 54 | const price = await peth.joinPrice(3); 55 | expect(price).toEqual(WETH(3)); 56 | }); 57 | 58 | test('should return the exit price in weth', async () => { 59 | const price = await peth.exitPrice(2); 60 | expect(price).toEqual(WETH(2)); 61 | }); 62 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | 3 | function resolveNameForBoolean(role, bool, { defaults, disabled }) { 4 | let name; 5 | if (bool) { 6 | name = defaults[role]; 7 | if (!name) throw new Error(`The "${role}" service has no default`); 8 | } else { 9 | name = disabled[role]; 10 | if (!name) throw new Error(`The "${role}" service cannot be disabled`); 11 | } 12 | return name; 13 | } 14 | 15 | export function standardizeConfig(role, config, resolver) { 16 | if (config instanceof Array) { 17 | if (typeof config[0] == 'boolean' && resolver) { 18 | return [resolveNameForBoolean(role, config[0], resolver), config[1]]; 19 | } 20 | return config; 21 | } 22 | let className, settings; 23 | 24 | switch (typeof config) { 25 | case 'string': 26 | // handle a string that refers to a class name 27 | className = config; 28 | settings = {}; 29 | break; 30 | case 'function': 31 | // handle a service constructor 32 | className = config; 33 | settings = {}; 34 | break; 35 | case 'object': 36 | // handle a settings object -- use the default version 37 | className = resolver ? resolveNameForBoolean(role, true, resolver) : true; 38 | settings = config; 39 | // TODO could also handle a service instance or constructor here 40 | break; 41 | case 'boolean': 42 | className = resolver 43 | ? resolveNameForBoolean(role, config, resolver) 44 | : config; 45 | settings = {}; 46 | break; 47 | default: 48 | throw new Error(`could not parse settings for ${role}:`, config); 49 | } 50 | 51 | return [className, settings]; 52 | } 53 | 54 | export function mergeServiceConfig(role, sink, source, resolver) { 55 | sink = standardizeConfig(role, sink, resolver); 56 | source = standardizeConfig(role, source); 57 | if (sink[0] === false || source[0] === false) return source; 58 | 59 | return [ 60 | typeof source[0] != 'boolean' ? source[0] : sink[0], 61 | merge({}, sink[1], source[1]) 62 | ]; 63 | } 64 | 65 | export function getSettings(config) { 66 | if (config instanceof Array) return config[1]; 67 | return config; 68 | } 69 | -------------------------------------------------------------------------------- /src/Maker.js: -------------------------------------------------------------------------------- 1 | import DefaultServiceProvider, { 2 | resolver 3 | } from './config/DefaultServiceProvider'; 4 | import ConfigFactory from './config/ConfigFactory'; 5 | 6 | export default class Maker { 7 | constructor(preset, options = {}) { 8 | const config = ConfigFactory.create(preset, options, resolver); 9 | this._container = new DefaultServiceProvider(config).buildContainer(); 10 | if (options.plugins) { 11 | for (let plugin of options.plugins) { 12 | plugin(this, config); 13 | } 14 | } 15 | if (options.autoAuthenticate !== false) this.authenticate(); 16 | 17 | delegateToServices(this, { 18 | accounts: [ 19 | 'addAccount', 20 | 'currentAccount', 21 | 'currentAddress', 22 | 'listAccounts', 23 | 'useAccount' 24 | ], 25 | cdp: ['getCdp', 'openCdp'], 26 | event: ['on'], 27 | token: ['getToken'] 28 | }); 29 | } 30 | 31 | authenticate() { 32 | if (!this._authenticatedPromise) { 33 | this._authenticatedPromise = this._container.authenticate(); 34 | } 35 | return this._authenticatedPromise; 36 | } 37 | 38 | // skipAuthCheck should only be set if you're sure you don't need the service 39 | // to be initialized yet, e.g. when setting up a plugin 40 | service(service, skipAuthCheck = false) { 41 | const skipAuthCheckForServices = ['event']; 42 | if ( 43 | !skipAuthCheck && 44 | !this._container.isAuthenticated && 45 | !skipAuthCheckForServices.includes(service) 46 | ) { 47 | throw new Error( 48 | `Can't use service ${service} before authenticate() has finished.` 49 | ); 50 | } 51 | return this._container.service(service); 52 | } 53 | } 54 | 55 | function delegateToServices(maker, services) { 56 | for (const serviceName in services) { 57 | for (const methodName of services[serviceName]) { 58 | maker[methodName] = (...args) => 59 | maker.service(serviceName)[methodName](...args); 60 | } 61 | } 62 | } 63 | 64 | // This factory function doesn't do much at the moment, but it will give us 65 | // more flexibility for plugins and extensions in the future. 66 | Maker.create = function(...args) { 67 | return new Maker(...args); 68 | }; 69 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: ['./src/bundle/index.js'], 8 | output: { 9 | path: path.join(process.cwd(), 'dist'), 10 | filename: '[name].[hash].js', 11 | sourceMapFilename: '[name].[hash].js.map', 12 | library: '@makerdao/dai', 13 | libraryTarget: 'umd', 14 | }, 15 | resolve: { 16 | extensions: ['.js', '.json'], 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | exclude: /node_modules/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: ['env', 'stage-2'], 27 | // plugins: [ 28 | // "syntax-async-functions", 29 | // "transform-regenerator" 30 | // ] 31 | }, 32 | }, 33 | }, 34 | { 35 | test: /\.json$/, 36 | loader: 'json-loader', 37 | }, 38 | { 39 | enforce: 'pre', //to check source files, not modified by other loaders (like babel-loader) 40 | test: /\.js$/, 41 | exclude: /node_modules/, 42 | loader: 'eslint-loader', 43 | options: { 44 | 'ignorePattern': '**/*.scss' 45 | } 46 | } 47 | ] 48 | }, 49 | plugins: [ 50 | new HtmlWebpackPlugin({ 51 | title: 'Maker.js demo', 52 | template: 'src/bundle/index.html', 53 | inject: 'head' 54 | }), 55 | new webpack.LoaderOptionsPlugin({ 56 | minimize: true, 57 | debug: false 58 | }), 59 | new webpack.DefinePlugin({ 60 | 'process.env': { 61 | 'NODE_ENV': JSON.stringify('production') 62 | } 63 | }), 64 | new UglifyJSPlugin({ uglifyOptions: { 65 | beautify: false, 66 | mangle: { 67 | keep_fnames: true 68 | }, 69 | comments: false 70 | } 71 | }) 72 | ], 73 | externals: [ 74 | 'child_process' 75 | ] 76 | }; 77 | 78 | if (process.env.ANALYZE_BUNDLE) { 79 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 80 | module.exports.plugins.push(new BundleAnalyzerPlugin()); 81 | } 82 | -------------------------------------------------------------------------------- /test/utils/TimerService.spec.js: -------------------------------------------------------------------------------- 1 | import TimerService from '../../src/utils/TimerService'; 2 | 3 | const DURATION_SHORT = 10, 4 | DURATION_LONG = 20; 5 | 6 | test('allows setting repeating timers', done => { 7 | const timer = new TimerService(); 8 | let calls = 0; 9 | 10 | timer.createTimer('repeater', 1, true, () => { 11 | calls += 1; 12 | 13 | if (calls >= 3) { 14 | // Allow failure if this isn't disposed of. 15 | done(); 16 | timer.disposeTimer('repeater'); 17 | } 18 | }); 19 | }); 20 | 21 | test('allows setting non-repeating timers', done => { 22 | const timer = new TimerService(); 23 | timer.createTimer('once', 1, false, done); 24 | }); 25 | 26 | test('only allow one timer per name, disposing on collision', done => { 27 | const timer = new TimerService(); 28 | timer.createTimer('collide', DURATION_SHORT, false, done.fail); 29 | timer.createTimer('collide', DURATION_LONG, false, done); 30 | }); 31 | 32 | test('only allow one repeating timer per name, disposing on collision', done => { 33 | const timer = new TimerService(); 34 | timer.createTimer('collide', DURATION_SHORT, true, done.fail); 35 | timer.createTimer('collide', DURATION_LONG, true, () => { 36 | done(); 37 | timer.disposeTimer('collide'); 38 | }); 39 | }); 40 | 41 | test('dispose of timers by name', done => { 42 | const timer = new TimerService(); 43 | timer.createTimer('fail', DURATION_SHORT, false, done.fail); 44 | timer.createTimer('pass', DURATION_LONG, false, done); 45 | timer.disposeTimer('fail'); 46 | }); 47 | 48 | test('dispose of all timers', done => { 49 | const timer = new TimerService(); 50 | timer.createTimer('fail1', DURATION_SHORT, false, done.fail); 51 | timer.createTimer('fail2', DURATION_SHORT, false, done.fail); 52 | timer.createTimer('fail3', DURATION_SHORT, false, done.fail); 53 | timer.disposeAllTimers(); 54 | 55 | timer.createTimer('pass', DURATION_LONG, false, done); 56 | }); 57 | 58 | test('list all timers', () => { 59 | const timer = new TimerService(); 60 | timer.createTimer('a', DURATION_SHORT, false, () => {}); 61 | timer.createTimer('b', DURATION_SHORT, false, () => {}); 62 | timer.createTimer('c', DURATION_SHORT, false, () => {}); 63 | expect(timer.listTimers()).toEqual(['a', 'b', 'c']); 64 | timer.disposeAllTimers(); 65 | }); 66 | -------------------------------------------------------------------------------- /src/utils/events/EventService.js: -------------------------------------------------------------------------------- 1 | import PrivateService from '../../core/PrivateService'; 2 | import { slug } from '../index'; 3 | import EventEmitter from './EventEmitter'; 4 | 5 | export default class EventService extends PrivateService { 6 | /** 7 | * @param {string} name 8 | */ 9 | constructor(name = 'event') { 10 | super(name, ['log']); 11 | 12 | this._block = null; 13 | // all of our emitters – we can have many of these 14 | // e.g. one for our maker object, a couple for some cdp objects, a few more on transaction objects, etc 15 | this.emitters = {}; 16 | 17 | // this is our default emitter, it will likely be the maker object's personal emitter 18 | this.buildEmitter({ defaultEmitter: true }); 19 | 20 | this.ping = this.ping.bind(this); 21 | } 22 | 23 | // check all of our active polls for new state 24 | // this is currently called on every new block from Web3Service 25 | ping(block) { 26 | Object.values(this.emitters).forEach(emitter => emitter.ping(block)); 27 | } 28 | 29 | // add a event listener to an emitter 30 | on(event, listener, emitter = this._defaultEmitter()) { 31 | emitter.on(event, listener); 32 | } 33 | 34 | // push an event through an emitter 35 | emit(event, payload, block, emitter = this._defaultEmitter()) { 36 | emitter.emit(event, payload, block); 37 | } 38 | 39 | // remove a listener from an emitter 40 | removeListener(event, listener, emitter = this._defaultEmitter()) { 41 | emitter.removeListener(event, listener); 42 | } 43 | 44 | registerPollEvents(eventPayloadMap, emitter = this._defaultEmitter()) { 45 | return emitter.registerPollEvents(eventPayloadMap); 46 | } 47 | 48 | buildEmitter({ defaultEmitter = false } = {}) { 49 | const id = defaultEmitter ? 'default' : slug(); 50 | const disposeEmitter = this._disposeEmitter.bind(this, id); 51 | const newEmitter = new EventEmitter(disposeEmitter); 52 | newEmitter.on('error', eventObj => this._logError(id, eventObj.payload)); 53 | this.emitters[id] = newEmitter; 54 | return newEmitter; 55 | } 56 | 57 | _disposeEmitter(id) { 58 | if (id === 'default') { 59 | this._logError(id, 'cannot dispose default emitter'); 60 | } else delete this.emitters[id]; 61 | } 62 | 63 | _defaultEmitter() { 64 | return this.emitters.default; 65 | } 66 | 67 | _logError(name, msg) { 68 | this.get('log').error(`Problem encountered in emitter ${name} -> ${msg}`); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /contracts/abis/DSGuard.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"sig","type":"bytes32"}],"name":"forbid","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"bytes32"},{"name":"dst","type":"bytes32"},{"name":"sig","type":"bytes32"}],"name":"forbid","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"ANY","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"src_","type":"address"},{"name":"dst_","type":"address"},{"name":"sig","type":"bytes4"}],"name":"canCall","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"sig","type":"bytes32"}],"name":"permit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"bytes32"},{"name":"dst","type":"bytes32"},{"name":"sig","type":"bytes32"}],"name":"permit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"bytes32"},{"indexed":true,"name":"dst","type":"bytes32"},{"indexed":true,"name":"sig","type":"bytes32"}],"name":"LogPermit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"bytes32"},{"indexed":true,"name":"dst","type":"bytes32"},{"indexed":true,"name":"sig","type":"bytes32"}],"name":"LogForbid","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /src/eth/accounts/setup.js: -------------------------------------------------------------------------------- 1 | import ProviderType from '../web3/ProviderType'; 2 | import Web3ProviderEngine from 'web3-provider-engine/dist/es5'; 3 | import RpcSource from 'web3-provider-engine/dist/es5/subproviders/rpc'; 4 | import ProviderSubprovider from 'web3-provider-engine/dist/es5/subproviders/provider'; 5 | 6 | export async function setupEngine(settings) { 7 | const { provider: providerSettings } = settings.web3; 8 | const engine = new Web3ProviderEngine(); 9 | const result = { engine }; 10 | 11 | if (providerSettings.type === ProviderType.BROWSER || !providerSettings) { 12 | result.provider = await getBrowserProvider(); 13 | } else { 14 | const rpcUrl = getRpcUrl(providerSettings); 15 | result.provider = new RpcSource({ rpcUrl }); 16 | } 17 | 18 | engine.addProvider(result.provider); 19 | return result; 20 | } 21 | 22 | export async function getBrowserProvider() { 23 | if (typeof window === 'undefined') { 24 | throw new Error( 25 | 'Cannot use ProviderType.BROWSER because window is undefined' 26 | ); 27 | } 28 | 29 | const wrap = provider => { 30 | const subprovider = new ProviderSubprovider(provider); 31 | subprovider.isWindowProvider = true; 32 | return subprovider; 33 | }; 34 | 35 | // If web3 is injected (old MetaMask)... 36 | if (typeof window.web3 !== 'undefined') { 37 | return wrap(window.web3.currentProvider); 38 | } 39 | 40 | // If web3 is not injected (new MetaMask)... 41 | return new Promise((resolve, reject) => { 42 | let resolved = false; 43 | 44 | window.addEventListener('message', ({ data }) => { 45 | if (data && data.type && data.type === 'ETHEREUM_PROVIDER_SUCCESS') { 46 | resolved = true; 47 | resolve(wrap(window.ethereum)); 48 | } 49 | }); 50 | 51 | // Request provider 52 | window.postMessage({ type: 'ETHEREUM_PROVIDER_REQUEST' }, '*'); 53 | 54 | setTimeout(() => { 55 | if (!resolved) reject(new Error('Timed out waiting for provider')); 56 | }, 30000); 57 | }); 58 | } 59 | 60 | function getRpcUrl(providerSettings) { 61 | const { network, infuraApiKey, type, url } = providerSettings; 62 | switch (type) { 63 | case ProviderType.HTTP: 64 | return url; 65 | case ProviderType.INFURA: 66 | return `https://${network}.infura.io/${infuraApiKey || ''}`; 67 | case ProviderType.TEST: 68 | return 'http://127.1:2000'; 69 | default: 70 | throw new Error('Invalid web3 provider type: ' + type); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/exchanges/oasis/OasisOrder.js: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers'; 2 | import { DAI } from '../../eth/Currency'; 3 | 4 | export default class OasisOrder { 5 | constructor() { 6 | this._fillAmount = null; 7 | this._hybrid = null; 8 | } 9 | 10 | fillAmount() { 11 | return this._fillAmount; 12 | } 13 | 14 | fees() { 15 | return this._hybrid.getOriginalTransaction().fees(); 16 | } 17 | 18 | created() { 19 | return this._hybrid.getOriginalTransaction().timestamp(); 20 | } 21 | 22 | transact(oasisContract, transaction, transactionManager) { 23 | return transactionManager.createHybridTx(transaction, { 24 | businessObject: this, 25 | parseLogs: receiptLogs => { 26 | const LogTradeEvent = oasisContract.interface.events.LogTrade; 27 | const LogTradeTopic = utils.keccak256( 28 | transactionManager.get('web3')._web3.toHex(LogTradeEvent.signature) 29 | ); //find a way to convert string to hex without web3 30 | const receiptEvents = receiptLogs.filter(e => { 31 | return ( 32 | e.topics[0].toLowerCase() === LogTradeTopic.toLowerCase() && 33 | e.address.toLowerCase() === oasisContract.address.toLowerCase() 34 | ); 35 | }); 36 | let total = utils.bigNumberify('0'); 37 | receiptEvents.forEach(event => { 38 | const parsedLog = LogTradeEvent.parse(event.data); 39 | total = total.add(parsedLog[this._logKey]); 40 | }); 41 | this._fillAmount = this._unit.wei(total.toString()); 42 | } 43 | }); 44 | } 45 | } 46 | 47 | export class OasisBuyOrder extends OasisOrder { 48 | constructor() { 49 | super(); 50 | this._logKey = 'buy_amt'; 51 | this._unit = DAI; 52 | } 53 | 54 | static build(oasisContract, transaction, transactionManager) { 55 | const order = new OasisBuyOrder(); 56 | order._hybrid = order.transact( 57 | oasisContract, 58 | transaction, 59 | transactionManager 60 | ); 61 | return order._hybrid; 62 | } 63 | } 64 | 65 | export class OasisSellOrder extends OasisOrder { 66 | constructor(currency) { 67 | super(); 68 | this._logKey = 'pay_amt'; 69 | this._unit = currency; 70 | } 71 | 72 | static build(oasisContract, transaction, transactionManager, currency) { 73 | const order = new OasisSellOrder(currency); 74 | order._hybrid = order.transact( 75 | oasisContract, 76 | transaction, 77 | transactionManager 78 | ); 79 | return order._hybrid; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/core/StateMachine.js: -------------------------------------------------------------------------------- 1 | export class IllegalStateError extends Error {} 2 | 3 | export default class StateMachine { 4 | constructor(initialState, transitions) { 5 | if (typeof transitions !== 'object') { 6 | throw new Error('StateMachine transitions parameter must be an object.'); 7 | } 8 | 9 | if ( 10 | Object.keys(transitions).filter( 11 | k => transitions.hasOwnProperty(k) && !(transitions[k] instanceof Array) 12 | ).length > 0 13 | ) { 14 | throw new Error('Illegal StateMachine transition found: not an array.'); 15 | } 16 | 17 | if ( 18 | Object.keys(transitions).filter( 19 | k => 20 | transitions.hasOwnProperty(k) && 21 | transitions[k].filter(s => !transitions[s]).length > 0 22 | ).length > 0 23 | ) { 24 | throw new Error( 25 | 'Illegal StateMachine transition found: target state not in transition map.' 26 | ); 27 | } 28 | 29 | if (!(transitions[initialState] instanceof Array)) { 30 | throw new Error( 31 | 'Initial state ' + initialState + ' is not set in the transitions map.' 32 | ); 33 | } 34 | 35 | this._state = initialState; 36 | this._nextStates = transitions; 37 | this._stateChangedHandlers = []; 38 | } 39 | 40 | onStateChanged(callback) { 41 | this._stateChangedHandlers.push(callback); 42 | } 43 | 44 | state() { 45 | return this._state; 46 | } 47 | 48 | inState(state) { 49 | if (!(state instanceof Array)) { 50 | state = [state]; 51 | } 52 | 53 | return state.indexOf(this._state) >= 0; 54 | } 55 | 56 | assertState(state, operation = '') { 57 | if (!this.inState(state)) { 58 | throw new IllegalStateError( 59 | 'Illegal operation for state ' + 60 | this._state + 61 | (operation.length > 0 ? ': ' + operation : '') 62 | ); 63 | } 64 | } 65 | 66 | transitionTo(newState) { 67 | if (this._nextStates[newState] === undefined) { 68 | throw new IllegalStateError('Cannot set illegal state: ' + newState); 69 | } 70 | 71 | if (newState !== this._state) { 72 | if (this._nextStates[this._state].indexOf(newState) < 0) { 73 | throw new IllegalStateError( 74 | 'Illegal state transition: ' + this._state + ' to ' + newState 75 | ); 76 | } 77 | 78 | const oldState = this._state; 79 | this._state = newState; 80 | this._stateChangedHandlers.forEach(cb => cb(oldState, newState)); 81 | } 82 | 83 | return this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/events/EventEmitter.js: -------------------------------------------------------------------------------- 1 | import { createMemoizedPoll, createPayloadFetcher } from './helpers'; 2 | import EventEmitterObj from 'eventemitter2'; 3 | const { EventEmitter2 } = EventEmitterObj; 4 | 5 | export default class EventEmitter { 6 | constructor(disposeSelf) { 7 | this._emitter = new EventEmitter2({ 8 | wildcard: true, 9 | delimiter: '/' 10 | }); 11 | this._polls = []; 12 | this._block = null; 13 | this._sequenceNum = 1; 14 | this._disposeSelf = disposeSelf; 15 | this.emit = this.emit.bind(this); 16 | } 17 | 18 | emit(event, payload = {}, block = this._getBlock()) { 19 | // if nobody's listening for this event, don't actually emit it 20 | if (this._emitter.listeners(event).length === 0) return; 21 | const eventObj = { 22 | payload, 23 | block, 24 | type: event, 25 | sequence: this._sequenceNum 26 | }; 27 | this._sequenceNum++; 28 | this._emitter.emit(event, eventObj); 29 | } 30 | 31 | on(event, listener) { 32 | this._emitter.on(event, listener); 33 | // start polling for state changes if the associated event now has a listener 34 | this._polls.forEach( 35 | poll => this._emitter.listeners(poll.type()).length > 0 && poll.heat() 36 | ); 37 | } 38 | 39 | removeListener(event, listener) { 40 | this._emitter.removeListener(event, listener); 41 | // stop polling for state changes if the associated event no longer has a listener 42 | this._polls.forEach( 43 | poll => this._emitter.listeners(poll.type()).length === 0 && poll.cool() 44 | ); 45 | } 46 | 47 | registerPollEvents(eventPayloadMap) { 48 | for (const [eventType, payloadGetterMap] of Object.entries( 49 | eventPayloadMap 50 | )) { 51 | const payloadFetcher = createPayloadFetcher(payloadGetterMap); 52 | const memoizedPoll = createMemoizedPoll({ 53 | type: eventType, 54 | emit: this.emit, 55 | getState: payloadFetcher 56 | }); 57 | this._polls.push(memoizedPoll); 58 | } 59 | return this; 60 | } 61 | 62 | ping(block) { 63 | this._setBlock(block); 64 | this._polls.forEach(poll => poll.ping()); 65 | } 66 | 67 | dispose() { 68 | this.emit = () => {}; 69 | this.on = () => {}; 70 | this._disposeSelf(); 71 | } 72 | 73 | _setBlock(block) { 74 | if (block !== undefined) this._block = block; 75 | } 76 | 77 | _getBlock() { 78 | return this._block; 79 | } 80 | 81 | // For testing 82 | 83 | _startPolls() { 84 | this._polls.forEach(poll => poll.heat()); 85 | } 86 | 87 | _stopPolls() { 88 | this._polls.forEach(poll => poll.cool()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/eth/PriceService.spec.js: -------------------------------------------------------------------------------- 1 | import { buildTestService } from '../helpers/serviceBuilders'; 2 | import { Currency, ETH, USD_ETH, USD_MKR } from '../../src/eth/Currency'; 3 | 4 | function buildTestPriceService() { 5 | return buildTestService('price', { price: true }); 6 | } 7 | 8 | test('should return current eth price', async () => { 9 | const service = buildTestPriceService(); 10 | 11 | await service.manager().authenticate(); 12 | const price = await service.getEthPrice(); 13 | expect(price).toEqual(USD_ETH(400)); 14 | }); 15 | 16 | test('should be able to set eth price', async () => { 17 | const service = buildTestPriceService(); 18 | await service.manager().authenticate(); 19 | await service.setEthPrice(100); 20 | expect(await service.getEthPrice()).toEqual(USD_ETH(100)); 21 | await service.setEthPrice(400); 22 | expect(await service.getEthPrice()).toEqual(USD_ETH(400)); 23 | }); 24 | 25 | test('should be able to get mkr price', done => { 26 | const service = buildTestPriceService(); 27 | 28 | service 29 | .manager() 30 | .authenticate() 31 | .then(() => { 32 | service.getMkrPrice().then(price => { 33 | expect(price.gt(USD_MKR(0))).toBeTruthy(); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | test('should be able to set mkr price', async () => { 40 | const service = buildTestPriceService(); 41 | 42 | await service.manager().authenticate(); 43 | await service.setMkrPrice('777'); 44 | const price = await service.getMkrPrice(); 45 | expect(price).toEqual(USD_MKR(777)); 46 | }); 47 | 48 | test('should return the peth price', done => { 49 | const service = buildTestPriceService(); 50 | 51 | service 52 | .manager() 53 | .authenticate() 54 | .then(() => { 55 | service.getPethPrice().then(value => { 56 | expect(value instanceof Currency).toBeTruthy(); 57 | done(); 58 | }); 59 | }); 60 | }); 61 | 62 | test('can read the weth to peth ratio', async () => { 63 | const service = buildTestPriceService(); 64 | await service.manager().authenticate(); 65 | const ratio = await service.getWethToPethRatio(); 66 | expect(ratio).toBeGreaterThan(0); 67 | }); 68 | 69 | test('_valueForContract', async () => { 70 | const service = buildTestPriceService(); 71 | await service.manager().authenticate(); 72 | const value = service._valueForContract('43', ETH); 73 | expect(value).toBe( 74 | '0x00000000000000000000000000000000000000000000000254beb02d1dcc0000' 75 | ); 76 | 77 | const value2 = service._valueForContract('78901', ETH); 78 | expect(value2).toBe( 79 | '0x0000000000000000000000000000000000000000000010b53b55f895f7b40000' 80 | ); 81 | }); 82 | -------------------------------------------------------------------------------- /test/config/DefaultServiceProvider.spec.js: -------------------------------------------------------------------------------- 1 | import DefaultServiceProvider from '../../src/config/DefaultServiceProvider'; 2 | import config from '../../src/config/presets/test'; 3 | import LocalService from '../../src/core/LocalService'; 4 | 5 | test('support services in mapping', () => { 6 | expect(new DefaultServiceProvider().supports('SmartContractService')).toBe( 7 | true 8 | ); 9 | }); 10 | 11 | test('do not support services not in mapping', () => { 12 | expect(new DefaultServiceProvider().supports('DoesNotExist')).toBe(false); 13 | }); 14 | 15 | test('add web3 config into accounts config', () => { 16 | const settings = { provider: { type: 'foo' } }; 17 | const web3configs = [settings, ['Web3Service', settings]]; 18 | 19 | for (let config of web3configs) { 20 | const provider = new DefaultServiceProvider({ 21 | web3: config, 22 | accounts: { metamask: { type: 'browser' } } 23 | }); 24 | 25 | expect(provider._config).toEqual({ 26 | web3: config, 27 | accounts: { 28 | metamask: { type: 'browser' }, 29 | web3: settings 30 | } 31 | }); 32 | } 33 | }); 34 | 35 | test('create a container from a service configuration', async () => { 36 | const container = new DefaultServiceProvider({ 37 | ...config, 38 | log: false 39 | }).buildContainer(); 40 | 41 | expect( 42 | Object.keys(container._services).indexOf('smartContract') 43 | ).toBeGreaterThan(-1); 44 | 45 | await container.authenticate(); 46 | expect( 47 | container 48 | .service('web3') 49 | .manager() 50 | .isAuthenticated() 51 | ).toBe(true); 52 | }); 53 | 54 | test('throw an error when config has an unsupported service', () => { 55 | const servicesCopy = { 56 | ...config, 57 | missingService: 'DoesNotExist' 58 | }; 59 | expect(() => 60 | new DefaultServiceProvider(servicesCopy).buildContainer() 61 | ).toThrow('Unsupported service in configuration: DoesNotExist'); 62 | }); 63 | 64 | test('constructor in config', () => { 65 | class FakeService extends LocalService { 66 | constructor(name = 'timer') { 67 | super(name); 68 | } 69 | } 70 | 71 | const container = new DefaultServiceProvider({ 72 | timer: FakeService 73 | }).buildContainer(); 74 | 75 | expect(container.service('timer') instanceof FakeService).toBeTruthy(); 76 | }); 77 | 78 | test('can define new service roles', () => { 79 | class FooService extends LocalService { 80 | constructor(name = 'foo') { 81 | super(name); 82 | } 83 | } 84 | const container = new DefaultServiceProvider({ 85 | foo: FooService 86 | }); 87 | 88 | const service = container.service('foo'); 89 | expect(service instanceof FooService).toBeTruthy(); 90 | }); 91 | -------------------------------------------------------------------------------- /contracts/abis/SaiVox.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[],"name":"prod","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"era","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"how","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"par","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"tell","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"way","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"param","type":"bytes32"},{"name":"val","type":"uint256"}],"name":"mold","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"fix","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"tune","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tau","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"par_","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /test/eth/TokenConversionService.spec.js: -------------------------------------------------------------------------------- 1 | import { buildTestService } from '../helpers/serviceBuilders'; 2 | import { ETH, PETH, WETH } from '../../src/eth/Currency'; 3 | 4 | async function buildTestTokenConversionService(maxAllowance = true) { 5 | const service = buildTestService('conversion', { 6 | allowance: maxAllowance ? true : { useMinimizeAllowancePolicy: true }, 7 | conversion: true 8 | }); 9 | 10 | await service.manager().authenticate(); 11 | return service; 12 | } 13 | 14 | test('should convert eth to weth', async () => { 15 | let initialBalance, initialEthBalance, owner, token, eth; 16 | const conversionService = await buildTestTokenConversionService(); 17 | const tokenService = conversionService.get('token'); 18 | owner = tokenService.get('web3').currentAccount(); 19 | token = tokenService.getToken(WETH); 20 | eth = tokenService.getToken(ETH); 21 | 22 | initialEthBalance = parseFloat(await eth.balanceOf(owner)); 23 | initialBalance = parseFloat(await token.balanceOf(owner)); 24 | await conversionService.convertEthToWeth('0.1'); 25 | const newEthBalance = await eth.balanceOf(owner); 26 | expect(parseFloat(initialEthBalance)).toBeGreaterThan( 27 | parseFloat(newEthBalance) 28 | ); 29 | const newBalance = await token.balanceOf(owner); 30 | expect(parseFloat(newBalance)).toBeCloseTo(initialBalance + 0.1); 31 | }); 32 | 33 | test('should convert weth to peth', async () => { 34 | const conversionService = await buildTestTokenConversionService(false); 35 | const tokenService = conversionService.get('token'); 36 | const owner = tokenService.get('web3').currentAccount(); 37 | const token = tokenService.getToken(PETH); 38 | 39 | const initialBalance = parseFloat(await token.balanceOf(owner)); 40 | await conversionService.convertEthToWeth('0.1'); 41 | await conversionService.convertWethToPeth('0.1'); 42 | const newBalance = await token.balanceOf(owner); 43 | expect(parseFloat(newBalance)).toBeCloseTo(initialBalance + 0.1); 44 | }); 45 | 46 | test('should convert eth to peth', async done => { 47 | let initialBalance; 48 | const conversionService = await buildTestTokenConversionService(false); 49 | conversionService 50 | .manager() 51 | .authenticate() 52 | .then(() => { 53 | const tokenService = conversionService.get('token'); 54 | const owner = tokenService.get('web3').currentAccount(); 55 | const token = tokenService.getToken(PETH); 56 | 57 | token 58 | .balanceOf(owner) 59 | .then(balance => (initialBalance = parseFloat(balance))); 60 | conversionService.convertEthToPeth('0.1').then(() => { 61 | token.balanceOf(owner).then(newBalance => { 62 | expect(parseFloat(newBalance)).toBeCloseTo(initialBalance + 0.1); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/utils/loggers/BunyanLogger.js: -------------------------------------------------------------------------------- 1 | import ServiceManager from '../../core/ServiceManager'; 2 | import PrivateService from '../../core/PrivateService'; 3 | import bunyan from 'bunyan'; 4 | 5 | /** 6 | * @returns {string} 7 | * @private 8 | */ 9 | function _guid() { 10 | function s4() { 11 | return Math.floor((1 + Math.random()) * 0x10000) 12 | .toString(16) 13 | .substring(1); 14 | } 15 | return ( 16 | s4() + 17 | s4() + 18 | '-' + 19 | s4() + 20 | '-' + 21 | s4() + 22 | '-' + 23 | s4() + 24 | '-' + 25 | s4() + 26 | s4() + 27 | s4() 28 | ); 29 | } 30 | 31 | function _appendState(service) { 32 | return args => { 33 | if (typeof args[0] === 'object') { 34 | args[0].state = service.manager().state(); 35 | } else { 36 | args = [{ state: service.manager().state() }].concat(args); 37 | } 38 | 39 | return args; 40 | }; 41 | } 42 | 43 | export default class BunyanLogger extends PrivateService { 44 | /** 45 | * @param {string} name 46 | */ 47 | constructor(name = 'log') { 48 | super(name); 49 | 50 | this._logger = bunyan.createLogger({ 51 | name: 'makerdao', 52 | level: 'debug', 53 | serializers: { err: bunyan.stdSerializers.err }, 54 | client: _guid() 55 | }); 56 | } 57 | 58 | serviceLogger(service, name = null) { 59 | if (!ServiceManager.isValidService(service)) { 60 | throw new Error('Invalid service object'); 61 | } 62 | 63 | const log = this._logger.child({ svc: name || service.manager().name() }), 64 | append = _appendState(service), 65 | wrapper = { 66 | trace: (...args) => log.trace.apply(log, append(args)), 67 | debug: (...args) => log.debug.apply(log, append(args)), 68 | info: (...args) => log.info.apply(log, append(args)), 69 | warn: (...args) => log.warn.apply(log, append(args)), 70 | error: (...args) => log.error.apply(log, append(args)), 71 | fatal: (...args) => log.fatal.apply(log, append(args)) 72 | }; 73 | 74 | service.manager().onStateChanged((from, to) => { 75 | wrapper.debug({ transition: { from, to } }, 'State changed.'); 76 | }); 77 | 78 | return wrapper; 79 | } 80 | 81 | trace(...args) { 82 | this._logger.trace.apply(this._logger, args); 83 | } 84 | 85 | debug(...args) { 86 | this._logger.debug.apply(this._logger, args); 87 | } 88 | 89 | info(...args) { 90 | this._logger.info.apply(this._logger, args); 91 | } 92 | 93 | warn(...args) { 94 | this._logger.warn.apply(this._logger, args); 95 | } 96 | 97 | error(...args) { 98 | this._logger.error.apply(this._logger, args); 99 | } 100 | 101 | fatal(...args) { 102 | this._logger.fatal.apply(this._logger, args); 103 | } 104 | 105 | connect() {} 106 | 107 | authenticate() {} 108 | } 109 | -------------------------------------------------------------------------------- /src/config/DefaultServiceProvider.js: -------------------------------------------------------------------------------- 1 | import AccountsService from '../eth/AccountsService'; 2 | import AllowanceService from '../eth/AllowanceService'; 3 | import CacheService from '../utils/CacheService'; 4 | import ConsoleLogger from '../utils/loggers/ConsoleLogger'; 5 | import EthereumCdpService from '../eth/EthereumCdpService'; 6 | import EthereumTokenService from '../eth/EthereumTokenService'; 7 | import EventService from '../utils/events/EventService'; 8 | import GasEstimatorService from '../eth/GasEstimatorService'; 9 | import NonceService from '../eth/NonceService'; 10 | import NullEventService from '../utils/events/NullEventService'; 11 | import NullLogger from '../utils/loggers/NullLogger'; 12 | import OasisExchangeService from '../exchanges/oasis/OasisExchangeService'; 13 | import PriceService from '../eth/PriceService'; 14 | import ServiceProvider from './ServiceProvider'; 15 | import SmartContractService from '../eth/SmartContractService'; 16 | import TimerService from '../utils/TimerService'; 17 | import TokenConversionService from '../eth/TokenConversionService'; 18 | import TransactionManager from '../eth/TransactionManager'; 19 | import Web3Service from '../eth/Web3Service'; 20 | import { getSettings } from './index'; 21 | 22 | export const resolver = { 23 | defaults: { 24 | accounts: 'AccountsService', 25 | allowance: 'AllowanceService', 26 | cache: 'CacheService', 27 | cdp: 'EthereumCdpService', 28 | conversion: 'TokenConversionService', 29 | event: 'EventService', 30 | // exchange: intentionally omitted 31 | gasEstimator: 'GasEstimatorService', 32 | log: 'ConsoleLogger', 33 | price: 'PriceService', 34 | smartContract: 'SmartContractService', 35 | timer: 'TimerService', 36 | token: 'EthereumTokenService', 37 | transactionManager: 'TransactionManager', 38 | web3: 'Web3Service', 39 | nonce: 'NonceService' 40 | }, 41 | disabled: { 42 | event: 'NullEventService', 43 | log: 'NullLogger' 44 | } 45 | }; 46 | 47 | export default class DefaultServiceProvider extends ServiceProvider { 48 | constructor(config = {}) { 49 | if (config.web3) { 50 | config = { 51 | ...config, 52 | accounts: { 53 | ...config.accounts, 54 | web3: getSettings(config.web3) 55 | } 56 | }; 57 | } 58 | 59 | super(config, { 60 | services: { 61 | AccountsService, 62 | AllowanceService, 63 | CacheService, 64 | ConsoleLogger, 65 | EthereumCdpService, 66 | EthereumTokenService, 67 | EventService, 68 | GasEstimatorService, 69 | NullEventService, 70 | NullLogger, 71 | OasisExchangeService, 72 | PriceService, 73 | SmartContractService, 74 | TimerService, 75 | TokenConversionService, 76 | TransactionManager, 77 | Web3Service, 78 | NonceService 79 | }, 80 | ...resolver 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /contracts/abis/DSRoles.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"getUserRoles","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"code","type":"address"},{"name":"sig","type":"bytes4"}],"name":"getCapabilityRoles","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"code","type":"address"},{"name":"sig","type":"bytes4"}],"name":"isCapabilityPublic","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"who","type":"address"},{"name":"role","type":"uint8"},{"name":"enabled","type":"bool"}],"name":"setUserRole","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"role","type":"uint8"},{"name":"code","type":"address"},{"name":"sig","type":"bytes4"},{"name":"enabled","type":"bool"}],"name":"setRoleCapability","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"},{"name":"role","type":"uint8"}],"name":"hasUserRole","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"caller","type":"address"},{"name":"code","type":"address"},{"name":"sig","type":"bytes4"}],"name":"canCall","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"code","type":"address"},{"name":"sig","type":"bytes4"},{"name":"enabled","type":"bool"}],"name":"setPublicCapability","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"who","type":"address"},{"name":"enabled","type":"bool"}],"name":"setRootUser","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"isUserRoot","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /src/eth/Cdp.js: -------------------------------------------------------------------------------- 1 | import contracts from '../../contracts/contracts'; 2 | import { utils as ethersUtils } from 'ethers'; 3 | import { USD } from './Currency'; 4 | 5 | export default class Cdp { 6 | constructor(cdpService, cdpId = null) { 7 | this._cdpService = cdpService; 8 | this._smartContractService = this._cdpService.get('smartContract'); 9 | this._transactionManager = this._smartContractService.get( 10 | 'transactionManager' 11 | ); 12 | 13 | if (!cdpId) { 14 | this._create(); 15 | } else { 16 | this.id = cdpId; 17 | } 18 | 19 | this._emitterInstance = this._cdpService.get('event').buildEmitter(); 20 | this.on = this._emitterInstance.on; 21 | this._emitterInstance.registerPollEvents({ 22 | COLLATERAL: { 23 | USD: () => this.getCollateralValue(USD), 24 | ETH: () => this.getCollateralValue() 25 | }, 26 | DEBT: { 27 | dai: () => this.getDebtValue() 28 | } 29 | }); 30 | } 31 | 32 | _create() { 33 | const tubContract = this._smartContractService.getContractByName( 34 | contracts.SAI_TUB 35 | ); 36 | 37 | const currentAccount = this._smartContractService 38 | .get('web3') 39 | .currentAccount(); 40 | 41 | const getId = new Promise(resolve => { 42 | tubContract.onlognewcup = function(address, cdpIdBytes32) { 43 | if (currentAccount.toLowerCase() == address.toLowerCase()) { 44 | this.removeListener(); 45 | resolve(ethersUtils.bigNumberify(cdpIdBytes32).toNumber()); 46 | } 47 | }; 48 | }); 49 | 50 | this._transactionObject = this._transactionManager.createHybridTx( 51 | (async () => { 52 | const [id, openResult] = await Promise.all([getId, tubContract.open()]); 53 | this.id = id; 54 | return openResult; 55 | })(), 56 | { 57 | businessObject: this, 58 | metadata: { contract: 'SAI_TUB', method: 'open' } 59 | } 60 | ); 61 | } 62 | 63 | transactionObject() { 64 | return this._transactionObject; 65 | } 66 | 67 | getId() { 68 | return Promise.resolve(this.id); 69 | } 70 | } 71 | 72 | // each of these methods just calls the method of the same name on the service 73 | // with the cdp's id as the first argument 74 | const passthroughMethods = [ 75 | 'bite', 76 | 'drawDai', 77 | 'freeEth', 78 | 'freePeth', 79 | 'getCollateralValue', 80 | 'getCollateralizationRatio', 81 | 'getDebtValue', 82 | 'getGovernanceFee', 83 | 'getInfo', 84 | 'getLiquidationPrice', 85 | 'give', 86 | 'isSafe', 87 | 'lockEth', 88 | 'lockPeth', 89 | 'lockWeth', 90 | 'shut', 91 | 'wipeDai' 92 | ]; 93 | 94 | Object.assign( 95 | Cdp.prototype, 96 | passthroughMethods.reduce((acc, name) => { 97 | acc[name] = function(...args) { 98 | return this._cdpService[name](this.id, ...args); 99 | }; 100 | return acc; 101 | }, {}) 102 | ); 103 | -------------------------------------------------------------------------------- /src/config/ServiceProvider.js: -------------------------------------------------------------------------------- 1 | import uniq from 'lodash.uniq'; 2 | import Container from '../core/Container'; 3 | import { standardizeConfig } from './index'; 4 | 5 | export default class ServiceProvider { 6 | constructor(config, { services, defaults, disabled }) { 7 | this._config = config; 8 | 9 | // all the service classes that this provider should support 10 | this._services = services; 11 | 12 | // the services (as string names) that should be used for each role by 13 | // default, or when that role is disabled 14 | this._resolver = { defaults, disabled }; 15 | } 16 | 17 | /** 18 | * @param {string} serviceName 19 | * @returns {boolean} 20 | */ 21 | supports(serviceName) { 22 | return !!this._services[serviceName]; 23 | } 24 | 25 | /** 26 | * @param {object} servicesConfig 27 | * @returns {Container} 28 | */ 29 | buildContainer() { 30 | const container = new Container(); 31 | 32 | for (let role in this._config) { 33 | const [service, settings] = standardizeConfig( 34 | role, 35 | this._config[role], 36 | this._resolver 37 | ); 38 | 39 | let instance; 40 | 41 | // each config can contain a service descriptor in one of several forms: 42 | if (typeof service == 'object') { 43 | // instance 44 | instance = service; 45 | } else if (typeof service == 'function') { 46 | // constructor 47 | instance = new service(); 48 | } else { 49 | // string 50 | if (!this.supports(service)) { 51 | throw new Error('Unsupported service in configuration: ' + service); 52 | } 53 | 54 | instance = new this._services[service](); 55 | } 56 | 57 | instance.manager().settings(settings); 58 | container.register(instance, role); 59 | } 60 | 61 | this._registerDependencies(container); 62 | container.injectDependencies(); 63 | this._container = container; 64 | return container; 65 | } 66 | 67 | _registerDependencies(container) { 68 | const names = container.getRegisteredServiceNames(); 69 | 70 | // get the names of all dependencies 71 | const allDeps = names.reduce((acc, name) => { 72 | const service = container.service(name); 73 | const deps = service.manager().dependencies(); 74 | return uniq(acc.concat(deps)); 75 | }, []); 76 | 77 | // filter out the ones that are already registered 78 | const newDeps = allDeps.filter(name => !names.includes(name)); 79 | if (newDeps.length === 0) return; 80 | 81 | // register any remaining ones 82 | for (let name of newDeps) { 83 | const className = this._resolver.defaults[name]; 84 | const ctor = this._services[className]; 85 | if (!ctor) throw new Error(`No service found for "${name}"`); 86 | container.register(new ctor(), name); 87 | } 88 | 89 | // repeat, to find any dependencies for services that were just added 90 | this._registerDependencies(container); 91 | } 92 | 93 | service(name) { 94 | if (!this._container) this.buildContainer(); 95 | return this._container.service(name); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/eth/AllowanceService.spec.js: -------------------------------------------------------------------------------- 1 | import TestAccountProvider from '../helpers/TestAccountProvider'; 2 | import { buildTestService } from '../helpers/serviceBuilders'; 3 | import { DAI } from '../../src/eth/Currency'; 4 | import { UINT256_MAX } from '../../src/utils/constants'; 5 | 6 | let dai, testAddress, allowanceService, owner; 7 | 8 | async function buildTestAllowanceService(max = true) { 9 | allowanceService = buildTestService('allowance', { 10 | allowance: max ? true : { useMinimizeAllowancePolicy: true } 11 | }); 12 | await allowanceService.manager().authenticate(); 13 | dai = allowanceService.get('token').getToken(DAI); 14 | owner = allowanceService 15 | .get('token') 16 | .get('web3') 17 | .currentAccount(); 18 | } 19 | 20 | beforeEach(() => { 21 | testAddress = TestAccountProvider.nextAddress(); 22 | }); 23 | 24 | afterEach(async () => { 25 | if (dai) await dai.approve(testAddress, 0); 26 | }); 27 | 28 | test('max allowance policy, no need to update', async () => { 29 | await buildTestAllowanceService(); 30 | await dai.approveUnlimited(testAddress); 31 | await allowanceService.requireAllowance(DAI, testAddress); 32 | 33 | const allowance = await dai.allowance(owner, testAddress); 34 | expect(allowance).toEqual(DAI.wei(UINT256_MAX)); 35 | }); 36 | 37 | test('max allowance policy, need to update', async () => { 38 | await buildTestAllowanceService(); 39 | await dai.approve(testAddress, 0); 40 | await allowanceService.requireAllowance(DAI, testAddress); 41 | 42 | const allowance = await dai.allowance(owner, testAddress); 43 | expect(allowance).toEqual(DAI.wei(UINT256_MAX)); 44 | }); 45 | 46 | test('min allowance policy, need to update', async () => { 47 | buildTestAllowanceService(false); 48 | const estimate = DAI(100); 49 | await dai.approve(testAddress, DAI(50)); 50 | await allowanceService.requireAllowance(DAI, testAddress, estimate); 51 | 52 | const allowance = await dai.allowance(owner, testAddress); 53 | expect(allowance).toEqual(estimate); 54 | }); 55 | 56 | test('min allowance policy, no need to update', async () => { 57 | await buildTestAllowanceService(false); 58 | const estimate = DAI(100); 59 | const initialAllowance = DAI(200); 60 | await dai.approve(testAddress, initialAllowance); 61 | await allowanceService.requireAllowance(DAI, testAddress, estimate); 62 | 63 | const allowance = await dai.allowance(owner, testAddress); 64 | expect(allowance).toEqual(initialAllowance); 65 | }); 66 | 67 | test('removeAllowance() works, need to update', async () => { 68 | await buildTestAllowanceService(false); 69 | await dai.approve(testAddress, 300); 70 | await allowanceService.removeAllowance(DAI, testAddress); 71 | 72 | const allowance = await dai.allowance(owner, testAddress); 73 | expect(allowance).toEqual(DAI(0)); 74 | }); 75 | 76 | test('removeAllowance() works, no need to update', async () => { 77 | await buildTestAllowanceService(false); 78 | await dai.approve(testAddress, 0); 79 | await allowanceService.removeAllowance(DAI, testAddress); 80 | 81 | const allowance = await dai.allowance(owner, testAddress); 82 | expect(allowance).toEqual(DAI(0)); 83 | }); 84 | -------------------------------------------------------------------------------- /test/eth/tokens/Erc20Token.spec.js: -------------------------------------------------------------------------------- 1 | import TestAccountProvider from '../../helpers/TestAccountProvider'; 2 | import { buildTestEthereumTokenService } from '../../helpers/serviceBuilders'; 3 | import { MKR, WETH } from '../../../src/eth/Currency'; 4 | import { UINT256_MAX } from '../../../src/utils/constants'; 5 | 6 | let tokenService, mkr, weth, currentAccount, testAddress; 7 | 8 | beforeAll(async () => { 9 | tokenService = buildTestEthereumTokenService(); 10 | await tokenService.manager().authenticate(); 11 | mkr = tokenService.getToken(MKR); 12 | weth = tokenService.getToken(WETH); 13 | currentAccount = tokenService.get('web3').currentAccount(); 14 | }); 15 | 16 | beforeEach(() => { 17 | testAddress = TestAccountProvider.nextAddress(); 18 | }); 19 | 20 | test('get ERC20 (MKR) balance of address', async () => { 21 | const balance = await mkr.balanceOf(TestAccountProvider.nextAddress()); 22 | expect(balance).toEqual(MKR(0)); 23 | }); 24 | 25 | test('get ERC20 (MKR) allowance of address', async () => { 26 | const allowance = await mkr.allowance( 27 | TestAccountProvider.nextAddress(), 28 | TestAccountProvider.nextAddress() 29 | ); 30 | expect(allowance).toEqual(MKR(0)); 31 | }); 32 | 33 | test('approve an ERC20 (MKR) allowance', async () => { 34 | await mkr.approve(testAddress, 10000); 35 | let allowance = await mkr.allowance(currentAccount, testAddress); 36 | expect(allowance).toEqual(MKR(10000)); 37 | 38 | await mkr.approve(testAddress, 0); 39 | allowance = await mkr.allowance(currentAccount, testAddress); 40 | expect(allowance).toEqual(MKR(0)); 41 | }); 42 | 43 | test('approveUnlimited an ERC20 (MKR) allowance', async () => { 44 | await mkr.approveUnlimited(testAddress); 45 | const allowance = await mkr.allowance(currentAccount, testAddress); 46 | expect(allowance).toEqual(MKR.wei(UINT256_MAX)); 47 | }); 48 | 49 | test('ERC20 transfer should move transferValue from sender to receiver', async () => { 50 | await weth.deposit('0.1'); 51 | const senderBalance = await weth.balanceOf(currentAccount); 52 | const receiverBalance = await weth.balanceOf(testAddress); 53 | 54 | await weth.transfer(testAddress, '0.1'); 55 | const newSenderBalance = await weth.balanceOf(currentAccount); 56 | const newReceiverBalance = await weth.balanceOf(testAddress); 57 | 58 | expect(newSenderBalance).toEqual(senderBalance.minus(0.1)); 59 | expect(newReceiverBalance).toEqual(receiverBalance.plus(0.1)); 60 | }); 61 | 62 | test('ERC20 transferFrom should move transferValue from sender to receiver', async () => { 63 | await weth.deposit('0.1'); 64 | const senderBalance = await weth.balanceOf(currentAccount); 65 | const receiverBalance = await weth.balanceOf(testAddress); 66 | 67 | await weth.transferFrom(currentAccount, testAddress, '0.1'); 68 | const newSenderBalance = await weth.balanceOf(currentAccount); 69 | const newReceiverBalance = await weth.balanceOf(testAddress); 70 | 71 | expect(newSenderBalance).toEqual(senderBalance.minus(0.1)); 72 | expect(newReceiverBalance).toEqual(receiverBalance.plus(0.1)); 73 | }); 74 | 75 | test('totalSupply() should increase when new tokens are minted', async () => { 76 | const supply1 = await weth.totalSupply(); 77 | await weth.deposit(0.1); 78 | const supply2 = await weth.totalSupply(); 79 | expect(supply1.plus(0.1)).toEqual(supply2); 80 | }); 81 | -------------------------------------------------------------------------------- /contracts/abis/SaiTop.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[],"name":"sin","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"skr","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"era","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"flow","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tub","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"cooldown_","type":"uint256"}],"name":"setCooldown","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vox","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"cage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"cooldown","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"gem","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"sai","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"fix","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"fit","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"caged","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"tap","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"tub_","type":"address"},{"name":"tap_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /src/eth/EthereumTokenService.js: -------------------------------------------------------------------------------- 1 | import PrivateService from '../core/PrivateService'; 2 | import tokens from '../../contracts/tokens'; 3 | import contracts from '../../contracts/contracts'; 4 | import networks from '../../contracts/networks'; 5 | import Erc20Token from './tokens/Erc20Token'; 6 | import EtherToken from './tokens/EtherToken'; 7 | import WethToken from './tokens/WethToken'; 8 | import PethToken from './tokens/PethToken'; 9 | 10 | export default class EthereumTokenService extends PrivateService { 11 | constructor(name = 'token') { 12 | super(name, [ 13 | 'smartContract', 14 | 'web3', 15 | 'log', 16 | 'gasEstimator', 17 | 'transactionManager' 18 | ]); 19 | } 20 | 21 | getTokens() { 22 | return Object.keys(tokens); 23 | } 24 | 25 | getTokenVersions() { 26 | const mapping = this._getCurrentNetworkMapping(); 27 | return this._selectTokenVersions(mapping); 28 | } 29 | 30 | getToken(symbol, version = null) { 31 | // support passing in Currency constructors 32 | if (symbol.symbol) symbol = symbol.symbol; 33 | 34 | if (this.getTokens().indexOf(symbol) < 0) { 35 | throw new Error('provided token is not a symbol'); 36 | } 37 | 38 | if (symbol === tokens.ETH) { 39 | return new EtherToken( 40 | this.get('web3'), 41 | this.get('gasEstimator'), 42 | this.get('transactionManager') 43 | ); 44 | } else { 45 | const mapping = this._getCurrentNetworkMapping(), 46 | tokenInfo = mapping[symbol], 47 | tokenVersionData = 48 | version === null 49 | ? tokenInfo[tokenInfo.length - 1] 50 | : tokenInfo[version - 1], 51 | smartContractService = this.get('smartContract'), 52 | contract = smartContractService.getContractByAddressAndAbi( 53 | tokenVersionData.address, 54 | tokenVersionData.abi 55 | ); 56 | 57 | if (symbol === tokens.WETH) { 58 | return new WethToken( 59 | contract, 60 | this.get('web3'), 61 | tokenVersionData.decimals 62 | ); 63 | } 64 | 65 | if (symbol === tokens.PETH) { 66 | if (tokenVersionData.decimals !== 18) { 67 | throw new Error('PethToken code hardcodes 18 decimal places.'); 68 | } 69 | const tub = smartContractService.getContractByName(contracts.SAI_TUB); 70 | return new PethToken(contract, this.get('web3'), tub); 71 | } 72 | 73 | return new Erc20Token( 74 | contract, 75 | this.get('web3'), 76 | tokenVersionData.decimals, 77 | symbol 78 | ); 79 | } 80 | } 81 | 82 | _getCurrentNetworkMapping() { 83 | let networkId = this.get('web3').networkId(); 84 | const mapping = networks.filter(m => m.networkId === networkId); 85 | 86 | if (mapping.length < 1) { 87 | /* istanbul ignore next */ 88 | throw new Error('networkId not found'); 89 | } 90 | 91 | return mapping[0].contracts; 92 | } 93 | 94 | _selectTokenVersions(mapping) { 95 | const tokenArray = []; 96 | 97 | for (let token in tokens) { 98 | if (token === 'ETH') { 99 | tokenArray['ETH'] = [1]; 100 | } 101 | 102 | if (token in mapping) { 103 | let versionArray = []; 104 | mapping[token].forEach(e => { 105 | versionArray.push(e.version); 106 | }); 107 | tokenArray[token] = versionArray; 108 | } 109 | } 110 | 111 | return tokenArray; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/eth/TransactionLifeCycle.js: -------------------------------------------------------------------------------- 1 | import StateMachine from '../core/StateMachine'; 2 | import transactionState from '../eth/TransactionState'; 3 | import TransactionType, { 4 | transactionTypeTransitions 5 | } from './TransactionTransitions'; 6 | 7 | const { initialized, pending, mined, finalized, error } = transactionState; 8 | const stateOrder = [initialized, pending, mined, finalized]; 9 | 10 | class TransactionLifeCycle { 11 | constructor(businessObject) { 12 | this._state = new StateMachine( 13 | initialized, 14 | transactionTypeTransitions[TransactionType.transaction] 15 | ); 16 | this._businessObject = businessObject; 17 | } 18 | 19 | setPending() { 20 | this._state.transitionTo(pending); 21 | } 22 | 23 | setMined() { 24 | this._state.transitionTo(mined); 25 | } 26 | 27 | setFinalized() { 28 | this._state.transitionTo(finalized); 29 | } 30 | 31 | setError(errorObject) { 32 | this.error = errorObject; 33 | this._state.transitionTo(error); 34 | } 35 | 36 | state() { 37 | return this._state.state(); 38 | } 39 | 40 | /** 41 | * @returns {boolean} 42 | */ 43 | isInitialized() { 44 | return this._state.inState(initialized); 45 | } 46 | 47 | /** 48 | * @returns {boolean} 49 | */ 50 | isPending() { 51 | return this._state.inState(pending); 52 | } 53 | 54 | /** 55 | * @returns {boolean|null} 56 | */ 57 | isMined() { 58 | return this._state.inState(mined); 59 | } 60 | 61 | /** 62 | * @returns {boolean|null} 63 | */ 64 | isFinalized() { 65 | return this._state.inState(finalized); 66 | } 67 | 68 | /** 69 | * @returns {boolean} 70 | */ 71 | isError() { 72 | return this._state.inState(error); 73 | } 74 | 75 | _returnValue() { 76 | return this._businessObject || this; 77 | } 78 | 79 | _inOrPastState(state) { 80 | const currentIndex = stateOrder.indexOf(this.state()); 81 | const targetIndex = stateOrder.indexOf(state); 82 | if (currentIndex === -1 || targetIndex === -1) { 83 | throw new Error('invalid state'); 84 | } 85 | return currentIndex >= targetIndex; 86 | } 87 | 88 | _onStateChange(from, to, handler) { 89 | if (this.isError()) return Promise.reject(this.error); 90 | if (this._inOrPastState(to)) return Promise.resolve(this._returnValue()); 91 | return new Promise((resolve, reject) => { 92 | this._state.onStateChanged((oldState, newState) => { 93 | if (oldState === from && newState === to) { 94 | if (handler) handler(this._returnValue()); 95 | resolve(this._returnValue()); 96 | } 97 | if (newState === error) reject(this.error); 98 | }); 99 | }); 100 | } 101 | 102 | onPending(handler) { 103 | return this._onStateChange(initialized, pending, handler); 104 | } 105 | 106 | onMined(handler) { 107 | return this._onStateChange(pending, mined, handler); 108 | } 109 | 110 | onFinalized(handler) { 111 | return this._onStateChange(mined, finalized, handler); 112 | } 113 | 114 | onError(handler) { 115 | if (this.isError()) return Promise.reject(); 116 | return new Promise((resolve, reject) => { 117 | this._state.onStateChanged((oldState, newState) => { 118 | if (newState === error) { 119 | if (handler) handler(this.error, this._returnValue()); 120 | reject(this.error, this._returnValue()); 121 | } 122 | }); 123 | }); 124 | } 125 | } 126 | 127 | export default TransactionLifeCycle; 128 | -------------------------------------------------------------------------------- /contracts/abis/SaiMom.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"setTubGap","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"setTapGap","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"setTax","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tub","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"setCap","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"vox","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"setMat","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"pip_","type":"address"}],"name":"setPip","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"setHow","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"setAxe","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"ray","type":"uint256"}],"name":"setWay","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"vox_","type":"address"}],"name":"setVox","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"pep_","type":"address"}],"name":"setPep","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"tap","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"tub_","type":"address"},{"name":"tap_","type":"address"},{"name":"vox_","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /contracts/abis/SaiProxyCreateAndExecute.json: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"},{"name":"wad","type":"uint256"}],"name":"draw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"},{"name":"jam","type":"uint256"},{"name":"wad","type":"uint256"},{"name":"otc_","type":"address"}],"name":"wipeAndFree","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"},{"name":"wad","type":"uint256"}],"name":"lockAndDraw","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"wad","type":"uint256"}],"name":"lockAndDraw","outputs":[{"name":"cup","type":"bytes32"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"registry_","type":"address"},{"name":"tub_","type":"address"}],"name":"createAndOpen","outputs":[{"name":"proxy","type":"address"},{"name":"cup","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"},{"name":"otc_","type":"address"}],"name":"shut","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"},{"name":"wad","type":"uint256"},{"name":"otc_","type":"address"}],"name":"wipe","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"},{"name":"wad","type":"uint256"}],"name":"wipe","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"}],"name":"open","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"}],"name":"shut","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"}],"name":"lock","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"registry_","type":"address"},{"name":"tub_","type":"address"},{"name":"wad","type":"uint256"}],"name":"createOpenLockAndDraw","outputs":[{"name":"proxy","type":"address"},{"name":"cup","type":"bytes32"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"},{"name":"lad","type":"address"}],"name":"give","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"registry_","type":"address"},{"name":"tub_","type":"address"}],"name":"createOpenAndLock","outputs":[{"name":"proxy","type":"address"},{"name":"cup","type":"bytes32"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"},{"name":"jam","type":"uint256"}],"name":"free","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"tub_","type":"address"},{"name":"cup","type":"bytes32"},{"name":"jam","type":"uint256"},{"name":"wad","type":"uint256"}],"name":"wipeAndFree","outputs":[],"payable":true,"stateMutability":"payable","type":"function"}] -------------------------------------------------------------------------------- /contracts/abis/DSChiefApprovals.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[],"name":"IOU","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"owner_","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"GOV","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MAX_YAYS","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"whom","type":"address"}],"name":"lift","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"yays","type":"address[]"}],"name":"etch","outputs":[{"name":"slate","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"approvals","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"authority_","type":"address"}],"name":"setAuthority","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"slate","type":"bytes32"}],"name":"vote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"authority","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"},{"name":"","type":"uint256"}],"name":"slates","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"votes","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"free","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"lock","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"yays","type":"address[]"}],"name":"vote","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"deposits","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"hat","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"GOV_","type":"address"},{"name":"IOU_","type":"address"},{"name":"MAX_YAYS_","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"slate","type":"bytes32"}],"name":"Etch","type":"event"},{"anonymous":true,"inputs":[{"indexed":true,"name":"sig","type":"bytes4"},{"indexed":true,"name":"guy","type":"address"},{"indexed":true,"name":"foo","type":"bytes32"},{"indexed":true,"name":"bar","type":"bytes32"},{"indexed":false,"name":"wad","type":"uint256"},{"indexed":false,"name":"fax","type":"bytes"}],"name":"LogNote","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"authority","type":"address"}],"name":"LogSetAuthority","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"LogSetOwner","type":"event"}] -------------------------------------------------------------------------------- /src/core/Container.js: -------------------------------------------------------------------------------- 1 | import values from 'lodash.values'; 2 | import ServiceManager, { InvalidServiceError } from './ServiceManager'; 3 | import toposort from 'toposort'; 4 | 5 | class ServiceAlreadyRegisteredError extends Error { 6 | constructor(name) { 7 | // prettier-ignore 8 | super('Service with name \'' + name + '\' is already registered with this container.'); 9 | } 10 | } 11 | 12 | class ServiceNotFoundError extends Error { 13 | constructor(name) { 14 | // prettier-ignore 15 | super('Service with name \'' + name + '\' cannot be found in this container.'); 16 | } 17 | } 18 | 19 | class ServiceDependencyLoopError extends Error { 20 | constructor(names) { 21 | super('Service dependency loop in {' + names.join(', ') + '}'); 22 | } 23 | } 24 | 25 | // exported just for testing 26 | export function orderServices(services) { 27 | const edges = []; 28 | for (let service of services) { 29 | const name = service.manager().name(); 30 | const depNames = service.manager().dependencies(); 31 | depNames.forEach(dn => edges.push([dn, name])); 32 | } 33 | return toposort(edges); 34 | } 35 | 36 | class Container { 37 | constructor() { 38 | this._services = {}; 39 | this.isAuthenticated = false; 40 | } 41 | 42 | register(service, name = null) { 43 | if (!ServiceManager.isValidService(service)) { 44 | throw new InvalidServiceError( 45 | 'Service must be an object with manager() method returning a valid ServiceManager' 46 | ); 47 | } 48 | 49 | name = name || service.manager().name(); 50 | 51 | const s = this.service(name, false); 52 | if (s !== undefined && s !== service) { 53 | throw new ServiceAlreadyRegisteredError(name); 54 | } 55 | 56 | this._services[name] = service; 57 | return this; 58 | } 59 | 60 | // export just this function 61 | service(name, throwIfMissing = true) { 62 | if (!name) { 63 | throw new Error('Provide a service name.'); 64 | } 65 | 66 | if (!this._services[name] && throwIfMissing) { 67 | throw new ServiceNotFoundError(name); 68 | } 69 | 70 | return this._services[name]; 71 | } 72 | 73 | getRegisteredServiceNames() { 74 | return Object.keys(this._services); 75 | } 76 | 77 | injectDependencies() { 78 | const services = values(this._services); 79 | for (let service of services) { 80 | const manager = service.manager(); 81 | for (let name of manager.dependencies()) { 82 | const dep = this._services[name]; 83 | if (!dep) throw new ServiceNotFoundError(name); 84 | manager.inject(name, this._services[name]); 85 | } 86 | } 87 | } 88 | 89 | initialize() { 90 | return this._waitForServices(s => s.manager().initialize()); 91 | } 92 | 93 | connect() { 94 | return this._waitForServices(s => s.manager().connect()); 95 | } 96 | 97 | authenticate() { 98 | return this._waitForServices(s => s.manager().authenticate()).then(() => { 99 | this.isAuthenticated = true; 100 | }); 101 | } 102 | 103 | async _waitForServices(callback) { 104 | if (!this._orderedServiceNames) { 105 | this._orderedServiceNames = orderServices(values(this._services)); 106 | } 107 | for (let name of this._orderedServiceNames) { 108 | const service = this._services[name]; 109 | if (!service) { 110 | throw new Error(`No service for ${name}`); 111 | } 112 | await callback(this._services[name]); 113 | } 114 | } 115 | } 116 | 117 | export { 118 | Container as default, 119 | InvalidServiceError, 120 | ServiceAlreadyRegisteredError, 121 | ServiceNotFoundError, 122 | ServiceDependencyLoopError 123 | }; 124 | -------------------------------------------------------------------------------- /test/eth/SmartContractService.spec.js: -------------------------------------------------------------------------------- 1 | import contracts from '../../contracts/contracts'; 2 | import tokens from '../../contracts/tokens'; 3 | import { 4 | buildTestService, 5 | buildTestSmartContractService 6 | } from '../helpers/serviceBuilders'; 7 | 8 | test('getContractByName should have proper error checking', done => { 9 | const service = buildTestSmartContractService(); 10 | 11 | expect(() => service.getContractByName('NOT_A_CONTRACT')).toThrow( 12 | 'Provided name "NOT_A_CONTRACT" is not a contract' 13 | ); 14 | expect(() => service.getContractByName(contracts.SAI_TOP)).toThrow( 15 | 'Cannot resolve network ID. Are you connected?' 16 | ); 17 | 18 | service 19 | .manager() 20 | .authenticate() 21 | .then(() => { 22 | expect(() => 23 | service.getContractByName(contracts.SAI_TOP, { version: 999 }) 24 | ).toThrow('Cannot find contract SAI_TOP, version 999'); 25 | done(); 26 | }); 27 | }); 28 | 29 | test('getContractByName should return a functioning contract', done => { 30 | const service = buildTestSmartContractService(); 31 | service 32 | .manager() 33 | .authenticate() 34 | .then(() => { 35 | // Read the PETH address by calling TOP.skr(). Confirm that it's the same as the configured address. 36 | service 37 | .getContractByName(contracts.SAI_TOP) 38 | .gem() 39 | .then(data => { 40 | expect(data.toString().toUpperCase()).toEqual( 41 | service.getContractByName(tokens.WETH).address.toUpperCase() 42 | ); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | test("should get a contract's public constant member values in a state object", done => { 49 | const service = buildTestSmartContractService(); 50 | service 51 | .manager() 52 | .authenticate() 53 | .then(() => service.getContractState(contracts.SAI_MOM)) 54 | .then(r => { 55 | expect(r).toEqual({ 56 | __self: '0x603d52d6ae2b98a49f8f32817ad4effe7e8a2502; SAI_MOM', 57 | authority: '0x4986C24C7f752C2ac2D738F1270639Dd9E9D7BF5', 58 | tub: '0xE82CE3D6Bf40F2F9414C8d01A35E3d9eb16a1761; SAI_TUB', 59 | vox: '0xE16bf7AaFeB33cC921d6D311E0ff33C4faA836dD; SAI_VOX', 60 | owner: '0x0000000000000000000000000000000000000000', 61 | tap: '0x6896659267C3C9FD055d764327199A98E571e00D; SAI_TAP' 62 | }); 63 | done(); 64 | }); 65 | }); 66 | 67 | test('should support recursive smart contract state inspection', done => { 68 | const service = buildTestSmartContractService(); 69 | service 70 | .manager() 71 | .authenticate() 72 | .then(() => service.getContractState(contracts.SAI_TOP, 5, true, [])) 73 | .then(top => { 74 | expect(top.tub.gem.symbol).toEqual('WETH'); 75 | done(); 76 | }); 77 | }); 78 | 79 | test('parameterized smart contract input', async () => { 80 | const mockContractDefinition = { 81 | address: '0xbeefed1bedded2dabbed3defaced4decade5dead', 82 | abi: [ 83 | { 84 | constant: true, 85 | inputs: [], 86 | name: 'foo', 87 | outputs: [{ name: '', type: 'bytes32' }], 88 | payable: false, 89 | stateMutability: 'view', 90 | type: 'function' 91 | } 92 | ] 93 | }; 94 | 95 | const service = buildTestService('smartContract', { 96 | smartContract: { 97 | addContracts: { 98 | mock: mockContractDefinition 99 | } 100 | } 101 | }); 102 | 103 | await service.manager().authenticate(); 104 | const contract = service.getContractByName('mock'); 105 | expect(contract.address).toEqual(mockContractDefinition.address); 106 | expect(typeof contract.foo).toBe('function'); 107 | }); 108 | -------------------------------------------------------------------------------- /src/eth/AccountsService.js: -------------------------------------------------------------------------------- 1 | import PublicService from '../core/PublicService'; 2 | import { map, omit, pick } from 'lodash/fp'; 3 | import invariant from 'invariant'; 4 | import { 5 | privateKeyAccountFactory, 6 | providerAccountFactory, 7 | browserProviderAccountFactory 8 | } from './accounts/factories'; 9 | import { setupEngine } from './accounts/setup'; 10 | import { AccountType } from '../utils/constants'; 11 | 12 | export default class AccountsService extends PublicService { 13 | constructor(name = 'accounts') { 14 | super(name, []); 15 | this._accounts = {}; 16 | this._accountFactories = { 17 | privateKey: privateKeyAccountFactory, 18 | provider: providerAccountFactory, 19 | browser: browserProviderAccountFactory 20 | }; 21 | } 22 | 23 | async initialize(settings = {}) { 24 | this._settings = omit('web3', settings); 25 | 26 | const result = await setupEngine(settings); 27 | this._engine = result.engine; 28 | this._provider = result.provider; 29 | } 30 | 31 | async connect() { 32 | const accountNames = Object.keys(this._settings); 33 | for (const name of accountNames) { 34 | await this.addAccount(name, this._settings[name]); 35 | } 36 | if (accountNames.length === 0) { 37 | await this.addAccount('default', { type: AccountType.PROVIDER }); 38 | } 39 | this._engine.start(); 40 | } 41 | 42 | getProvider() { 43 | return this._engine; 44 | } 45 | 46 | addAccountType(type, factory) { 47 | invariant( 48 | !this._accountFactories[type], 49 | `Account type "${type}" is already defined` 50 | ); 51 | this._accountFactories[type] = factory; 52 | } 53 | 54 | async addAccount(name, { type, ...otherSettings }) { 55 | invariant(this._engine, 'engine must be set up before adding an account'); 56 | if (this._accounts[name]) { 57 | throw new Error('An account with this name already exists.'); 58 | } 59 | const factory = this._accountFactories[type]; 60 | invariant(factory, `no factory for type "${type}"`); 61 | const accountData = await factory(otherSettings, this._provider); 62 | const account = { name, type, ...accountData }; 63 | this._accounts[name] = account; 64 | if (!this._currentAccount || name === 'default') { 65 | this.useAccount(name); 66 | } 67 | return account; 68 | } 69 | 70 | listAccounts() { 71 | return map(pick(['name', 'type', 'address']), this._accounts); 72 | } 73 | 74 | useAccount(name) { 75 | invariant(this._accounts[name], `No account found with name "${name}".`); 76 | 77 | if (this._currentAccount) { 78 | this._engine.stop(); 79 | this._engine.removeProvider(this.currentWallet()); 80 | } 81 | 82 | this._currentAccount = name; 83 | // add the provider at index 0 so that it takes precedence over RpcSource 84 | this._engine.addProvider(this.currentWallet(), 0); 85 | this._engine.start(); 86 | } 87 | 88 | hasAccount() { 89 | return !!this._currentAccount; 90 | } 91 | 92 | hasNonProviderAccount() { 93 | return ( 94 | this.hasAccount() && this.currentAccount().type != AccountType.PROVIDER 95 | ); 96 | } 97 | 98 | // we intentionally omit subprovider (implementation detail) and privateKey 99 | // (sensitive info). 100 | currentAccount() { 101 | invariant(this.hasAccount(), 'No account is set up.'); 102 | return pick( 103 | ['name', 'type', 'address'], 104 | this._accounts[this._currentAccount] 105 | ); 106 | } 107 | 108 | currentAddress() { 109 | invariant(this.hasAccount(), 'No account is set up.'); 110 | return this._accounts[this._currentAccount].address; 111 | } 112 | 113 | currentWallet() { 114 | return this._accounts[this._currentAccount].subprovider; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/config/ConfigFactory.js: -------------------------------------------------------------------------------- 1 | import test from './presets/test.json'; 2 | import kovan from './presets/kovan.json'; 3 | import http from './presets/http.json'; 4 | import mainnet from './presets/mainnet.json'; 5 | import browser from './presets/browser.json'; 6 | import merge from 'lodash.merge'; 7 | import intersection from 'lodash.intersection'; 8 | import { mergeServiceConfig } from './index'; 9 | import { AccountType } from '../utils/constants'; 10 | 11 | class ConfigPresetNotFoundError extends Error { 12 | constructor(message) { 13 | super('Cannot find configuration preset with name: ' + message); 14 | } 15 | } 16 | 17 | const serviceRoles = [ 18 | 'accounts', 19 | 'allowance', 20 | 'cdp', 21 | 'conversion', 22 | 'exchange', 23 | 'gasEstimator', 24 | 'log', 25 | 'price', 26 | 'smartContract', 27 | 'timer', 28 | 'token', 29 | 'transactionManager', 30 | 'web3', 31 | 'nonce' 32 | ]; 33 | 34 | function loadPreset(name) { 35 | if (typeof name == 'object') { 36 | return name; // for testing 37 | } 38 | 39 | let preset; 40 | switch (name) { 41 | case 'test': 42 | preset = test; 43 | break; 44 | case 'http': 45 | preset = http; 46 | break; 47 | case 'kovan': 48 | preset = kovan; 49 | break; 50 | case 'mainnet': 51 | preset = mainnet; 52 | break; 53 | case 'browser': 54 | preset = browser; 55 | break; 56 | default: 57 | throw new ConfigPresetNotFoundError(name); 58 | } 59 | // make a copy so we don't overwrite the original values 60 | return merge({}, preset); 61 | } 62 | 63 | const reservedWords = [ 64 | 'accounts', 65 | 'overrideMetamask', 66 | 'plugins', 67 | 'privateKey', 68 | 'provider', 69 | 'url' 70 | ]; 71 | 72 | export default class ConfigFactory { 73 | /** 74 | * @param {string} preset 75 | * @param {object} options 76 | */ 77 | static create(preset, options = {}, resolver) { 78 | if (typeof preset !== 'string') { 79 | options = preset; 80 | preset = options.preset; 81 | } 82 | 83 | const config = loadPreset(preset); 84 | const additionalServices = options.additionalServices || []; 85 | 86 | const usedReservedWords = intersection(additionalServices, reservedWords); 87 | if (usedReservedWords.length > 0) { 88 | throw new Error( 89 | 'The following words cannot be used as service role names: ' + 90 | usedReservedWords.join(', ') 91 | ); 92 | } 93 | 94 | for (let role of serviceRoles.concat(additionalServices)) { 95 | if (!(role in options)) continue; 96 | if (!(role in config)) { 97 | config[role] = options[role]; 98 | continue; 99 | } 100 | config[role] = mergeServiceConfig( 101 | role, 102 | config[role], 103 | options[role], 104 | resolver 105 | ); 106 | } 107 | 108 | // web3-specific convenience options 109 | if (config.web3) { 110 | const web3Settings = config.web3[1] || config.web3; 111 | if (!web3Settings.provider) web3Settings.provider = {}; 112 | 113 | if (options.url) { 114 | web3Settings.provider.url = options.url; 115 | } 116 | 117 | if (options.provider) { 118 | merge(web3Settings.provider, options.provider); 119 | } 120 | } 121 | 122 | // accounts-specific convenience option 123 | if (options.privateKey) { 124 | config.accounts = { 125 | ...config.accounts, 126 | default: { type: AccountType.PRIVATE_KEY, key: options.privateKey } 127 | }; 128 | } 129 | 130 | // default settings for transactions 131 | if (options.transactionSettings) { 132 | config.transactionSettings = options.transactionSettings; 133 | } 134 | 135 | return config; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/config/ConfigFactory.spec.js: -------------------------------------------------------------------------------- 1 | import testPreset from '../../src/config/presets/test'; 2 | import ConfigFactory from '../../src/config/ConfigFactory'; 3 | 4 | test('returns a preset by name', () => { 5 | expect(ConfigFactory.create('test')).toEqual(testPreset); 6 | }); 7 | 8 | test('throws an error when requesting a non-existing preset', () => { 9 | expect(() => ConfigFactory.create('does-not-exist')).toThrow( 10 | 'Cannot find configuration preset with name: does-not-exist' 11 | ); 12 | }); 13 | 14 | test('can take an options object in addition to a preset name', () => { 15 | const config = ConfigFactory.create('test', { log: false }); 16 | expect(config.log).toEqual(false); 17 | }); 18 | 19 | test('can take an options object as first argument', () => { 20 | const config = ConfigFactory.create({ preset: 'test', log: false }); 21 | expect(config.log).toEqual(false); 22 | }); 23 | 24 | test('it handles url, privateKey, provider, and web3 options', () => { 25 | const config = ConfigFactory.create( 26 | 'http', 27 | { 28 | url: 'http://foo.net', 29 | privateKey: '0xf00', 30 | provider: { 31 | timeout: 1000 32 | }, 33 | web3: { 34 | statusTimerDelay: 10000 35 | } 36 | }, 37 | { 38 | defaults: { 39 | web3: 'Web3Service' 40 | } 41 | } 42 | ); 43 | 44 | expect(config).toEqual({ 45 | accounts: { 46 | default: { 47 | type: 'privateKey', 48 | key: '0xf00' 49 | } 50 | }, 51 | web3: [ 52 | 'Web3Service', 53 | { 54 | statusTimerDelay: 10000, 55 | provider: { 56 | timeout: 1000, 57 | type: 'HTTP', 58 | url: 'http://foo.net' 59 | }, 60 | transactionSettings: { 61 | gasLimit: 4000000 62 | } 63 | } 64 | ], 65 | exchange: 'OasisExchangeService' 66 | }); 67 | }); 68 | 69 | test('it overwrites a service name', () => { 70 | const config = ConfigFactory.create('http', { exchange: 'OtherService' }); 71 | expect(config.exchange).toEqual(['OtherService', {}]); 72 | }); 73 | 74 | test('it adds service options', () => { 75 | const config = ConfigFactory.create('http', { exchange: { foo: 'bar' } }); 76 | expect(config.exchange).toEqual(['OasisExchangeService', { foo: 'bar' }]); 77 | }); 78 | 79 | test('it passes service options for an omitted service', () => { 80 | const config = ConfigFactory.create('http', { cdp: { foo: 'bar' } }); 81 | expect(config.cdp).toEqual({ foo: 'bar' }); 82 | }); 83 | 84 | test('it preserves the preset service name', () => { 85 | const preset = { log: 'BunyanLogger' }; 86 | const config = ConfigFactory.create({ preset, log: { verbose: true } }); 87 | expect(config.log).toEqual(['BunyanLogger', { verbose: true }]); 88 | }); 89 | 90 | test('skip unknown service roles', () => { 91 | const config = ConfigFactory.create('http', { 92 | foo: 'FooService' 93 | }); 94 | expect(config.foo).toBeFalsy(); 95 | }); 96 | 97 | test('should capture transaction settings', () => { 98 | const txSettings = { 99 | gasPrice: 12000000000, 100 | gasLimit: 4000000 101 | }; 102 | const config = ConfigFactory.create('http', { 103 | web3: { 104 | transactionSettings: txSettings 105 | } 106 | }); 107 | expect(config.web3[1].transactionSettings).toEqual(txSettings); 108 | }); 109 | 110 | test('allow new service roles if specified', () => { 111 | const config = ConfigFactory.create('http', { 112 | additionalServices: ['foo'], 113 | foo: 'FooService' 114 | }); 115 | expect(config.foo).toEqual('FooService'); 116 | }); 117 | 118 | test('reject invalid service roles', () => { 119 | expect(() => { 120 | ConfigFactory.create('http', { 121 | additionalServices: ['url'] 122 | }); 123 | }).toThrow(/cannot be used as service role names/); 124 | }); 125 | -------------------------------------------------------------------------------- /src/exchanges/oasis/OasisExchangeService.js: -------------------------------------------------------------------------------- 1 | import PrivateService from '../../core/PrivateService'; 2 | import { OasisBuyOrder, OasisSellOrder } from './OasisOrder'; 3 | import TransactionObject from '../../eth/TransactionObject'; 4 | import contracts from '../../../contracts/contracts'; 5 | import { UINT256_MAX } from '../../utils/constants'; 6 | import { getCurrency, DAI, WETH } from '../../eth/Currency'; 7 | 8 | export default class OasisExchangeService extends PrivateService { 9 | constructor(name = 'exchange') { 10 | super(name, [ 11 | 'cdp', 12 | 'smartContract', 13 | 'token', 14 | 'web3', 15 | 'log', 16 | 'gasEstimator', 17 | 'allowance', 18 | 'transactionManager' 19 | ]); 20 | } 21 | 22 | /* 23 | daiAmount: amount of Dai to sell 24 | currency: currency to buy 25 | minFillAmount: minimum amount of token being bought required. If this can't be met, the trade will fail 26 | */ 27 | async sellDai(amount, currency, minFillAmount = 0) { 28 | const oasisContract = this.get('smartContract').getContractByName( 29 | contracts.MAKER_OTC, 30 | { hybrid: false } 31 | ); 32 | const daiToken = this.get('token').getToken(DAI); 33 | const daiAddress = daiToken.address(); 34 | const buyToken = this.get('token').getToken(currency); 35 | const daiAmountEVM = daiValueForContract(amount); 36 | const minFillAmountEVM = daiValueForContract(minFillAmount); 37 | await this.get('allowance').requireAllowance(DAI, oasisContract.address); 38 | return OasisSellOrder.build( 39 | oasisContract, 40 | oasisContract.sellAllAmount( 41 | daiAddress, 42 | daiAmountEVM, 43 | buyToken.address(), 44 | minFillAmountEVM, 45 | { gasLimit: 300000 } 46 | ), 47 | this.get('transactionManager'), 48 | currency 49 | ); 50 | } 51 | 52 | /* 53 | daiAmount: amount of Dai to buy 54 | tokenSymbol: symbol of token to sell 55 | maxFillAmount: If the trade can't be done without selling more than the maxFillAmount of selling token, it will fail 56 | */ 57 | async buyDai(amount, tokenSymbol, maxFillAmount = UINT256_MAX) { 58 | const oasisContract = this.get('smartContract').getContractByName( 59 | contracts.MAKER_OTC, 60 | { hybrid: false } 61 | ); 62 | const daiToken = this.get('token').getToken(DAI); 63 | const daiAddress = daiToken.address(); 64 | const daiAmountEVM = daiValueForContract(amount); 65 | const maxFillAmountEVM = daiValueForContract(maxFillAmount); 66 | const sellTokenAddress = this.get('token') 67 | .getToken(tokenSymbol) 68 | .address(); 69 | await this.get('allowance').requireAllowance(WETH, oasisContract.address); 70 | return OasisBuyOrder.build( 71 | oasisContract, 72 | oasisContract.buyAllAmount( 73 | daiAddress, 74 | daiAmountEVM, 75 | sellTokenAddress, 76 | maxFillAmountEVM, 77 | { gasLimit: 300000 } 78 | ), 79 | this.get('transactionManager') 80 | ); 81 | } 82 | 83 | //only used to set up a limit order on the local testnet 84 | async offer( 85 | payAmount, 86 | payTokenAddress, 87 | buyAmount, 88 | buyTokenAddress, 89 | pos, 90 | overrides 91 | ) { 92 | const oasisContract = this.get('smartContract').getContractByName( 93 | contracts.MAKER_OTC, 94 | { hybrid: false } 95 | ); 96 | return new TransactionObject( 97 | oasisContract.offer( 98 | payAmount, 99 | payTokenAddress, 100 | buyAmount, 101 | buyTokenAddress, 102 | pos, 103 | overrides 104 | ), 105 | this.get('web3'), 106 | this.get('transactionManager').get('nonce') 107 | ); 108 | } 109 | } 110 | 111 | function daiValueForContract(amount) { 112 | return getCurrency(amount, DAI).toEthersBigNumber('wei'); 113 | } 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@makerdao/dai", 3 | "version": "0.8.0", 4 | "contributors": [ 5 | "Wouter Kampmann ", 6 | "Sean Brennan ", 7 | "Tyler Sorensen ", 8 | "Ethan Bennett ", 9 | "Lawrence Wang ", 10 | "Joshua Levine ", 11 | "Michael Elliot " 12 | ], 13 | "description": "JavaScript library for interacting with the Dai Stablecoin System.", 14 | "license": "MIT", 15 | "keywords": [ 16 | "makerdao", 17 | "mkr", 18 | "dai", 19 | "cdp", 20 | "eth", 21 | "margin" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/makerdao/dai.js/issues", 25 | "email": "wouter@makerdao.com" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/makerdao/dai.js.git" 30 | }, 31 | "module": "src/index.js", 32 | "main": "src/index.js", 33 | "scripts": { 34 | "build:frontend": "./scripts/build-frontend.sh", 35 | "build:backend": "./scripts/build-backend.sh", 36 | "build:backend:watch": "sane ./scripts/build-backend.sh src --wait=10", 37 | "coverage": "./scripts/run-testchain.sh --ci jest --runInBand --coverage", 38 | "lint": "eslint web src test", 39 | "precommit": "lint-staged", 40 | "prepush": "npm run test", 41 | "prettier": "prettier --write --single-quote '{src,web,test,contracts}/**/*.js'", 42 | "test": "./scripts/run-testchain.sh --ci jest --runInBand", 43 | "test:watch": "./scripts/run-testchain.sh --ci jest --watch --runInBand", 44 | "test:logs": "tail -f ganache.out | grep -v 'eth_blockNumber'", 45 | "test:net": "./scripts/run-testchain.sh" 46 | }, 47 | "lint-staged": { 48 | "./{src,web,test,contracts}/**/*.js": [ 49 | "prettier --single-quote --write", 50 | "git add", 51 | "eslint" 52 | ] 53 | }, 54 | "devDependencies": { 55 | "babel-cli": "^6.26.0", 56 | "babel-core": "^6.26.3", 57 | "babel-eslint": "^7.2.3", 58 | "babel-jest": "^22.4.3", 59 | "babel-loader": "^7.1.4", 60 | "babel-plugin-transform-runtime": "^6.23.0", 61 | "babel-preset-env": "^1.6.0", 62 | "babel-preset-stage-2": "^6.24.1", 63 | "copyfiles": "^2.0.0", 64 | "eslint": "^4.19.1", 65 | "eslint-loader": "^1.9.0", 66 | "ganache-cli": "^6.0.3", 67 | "html-webpack-plugin": "^2.30.1", 68 | "husky": "^0.14.3", 69 | "jest": "^23.6.0", 70 | "lint-staged": "^7.1.0", 71 | "node-fetch": "^2.2.0", 72 | "prettier": "^1.12.1", 73 | "sane": "^3.0.0", 74 | "solc": "^0.4.23", 75 | "uglifyjs-webpack-plugin": "^1.2.5", 76 | "webpack": "^3.11.0", 77 | "webpack-bundle-analyzer": "^2.13.1" 78 | }, 79 | "dependencies": { 80 | "babel-runtime": "^6.26.0", 81 | "bignumber.js": "^7.2.1", 82 | "bunyan": "^1.8.12", 83 | "chalk": "^2.4.1", 84 | "debug": "^3.1.0", 85 | "ethers": "^3.0.15", 86 | "ethers-web3-bridge": "0.0.1", 87 | "eventemitter2": "^5.0.1", 88 | "invariant": "^2.2.2", 89 | "lodash": "^4.17.10", 90 | "lodash.intersection": "^4.4.0", 91 | "lodash.isequal": "^4.5.0", 92 | "lodash.merge": "^4.6.1", 93 | "lodash.times": "^4.3.2", 94 | "lodash.uniq": "^4.5.0", 95 | "lodash.values": "^4.3.0", 96 | "promise-props": "^1.0.0", 97 | "toposort": "^2.0.2", 98 | "web3": "^0.20.6", 99 | "web3-provider-engine": "makerdao/provider-engine#remove-provider-dist" 100 | }, 101 | "jest": { 102 | "coverageReporters": [ 103 | "json", 104 | "lcov", 105 | "text-summary" 106 | ], 107 | "collectCoverageFrom": [ 108 | "src/**/*.js" 109 | ], 110 | "globalSetup": "/test/setup-global.js", 111 | "roots": [ 112 | "src", 113 | "test" 114 | ], 115 | "setupTestFrameworkScriptFile": "/test/setup-test.js" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/core/ServiceManager.js: -------------------------------------------------------------------------------- 1 | import ServiceManagerBase from './ServiceManagerBase'; 2 | 3 | /** 4 | * 5 | */ 6 | class InvalidServiceError extends Error { 7 | constructor(message) { 8 | super(message); 9 | } 10 | } 11 | 12 | /** 13 | * 14 | */ 15 | class UnknownDependencyError extends Error { 16 | constructor(service, dependency) { 17 | super( 18 | 'Injected service ' + dependency + ' is not a dependency of ' + service 19 | ); 20 | } 21 | } 22 | 23 | /** 24 | * 25 | */ 26 | class DependencyNotResolvedError extends Error { 27 | constructor(service, dependency) { 28 | super( 29 | 'Dependency ' + dependency + ' of service ' + service + ' is unavailable.' 30 | ); 31 | } 32 | } 33 | 34 | /** 35 | * @param callback 36 | * @returns {Promise} 37 | * @private 38 | */ 39 | function _waitForDependencies(callback) { 40 | return Promise.all( 41 | this.dependencies().map(dependency => callback(dependency)) 42 | ); 43 | } 44 | 45 | /** 46 | * 47 | */ 48 | class ServiceManager extends ServiceManagerBase { 49 | /** 50 | * @param {*} service 51 | * @returns {boolean} 52 | */ 53 | static isValidService(service) { 54 | return ( 55 | service !== null && 56 | typeof service === 'object' && 57 | typeof service.manager === 'function' && 58 | service.manager() instanceof ServiceManager 59 | ); 60 | } 61 | 62 | /** 63 | * @param {string} name 64 | * @param {string[]} dependencies 65 | * @param {function|null} init 66 | * @param {function|null} connect 67 | * @param {function|null} auth 68 | */ 69 | constructor( 70 | name, 71 | dependencies = [], 72 | init = null, 73 | connect = null, 74 | auth = null 75 | ) { 76 | super(init, connect, auth); 77 | if (!name) { 78 | throw new Error('Service name must not be empty.'); 79 | } 80 | 81 | this._name = name; 82 | this._dependencies = dependencies; 83 | this._injections = {}; 84 | dependencies.forEach(d => (this._injections[d] = null)); 85 | } 86 | 87 | name() { 88 | return this._name; 89 | } 90 | 91 | dependencies() { 92 | return this._dependencies; 93 | } 94 | 95 | inject(dependency, service) { 96 | if (typeof this._injections[dependency] === 'undefined') { 97 | throw new UnknownDependencyError(this.name(), dependency); 98 | } 99 | 100 | if (!ServiceManager.isValidService(service)) { 101 | throw new InvalidServiceError( 102 | 'Cannot inject invalid service in ' + this.name() 103 | ); 104 | } 105 | 106 | this._injections[dependency] = service; 107 | 108 | return this; 109 | } 110 | 111 | dependency(name) { 112 | if (this._injections[name] === null) { 113 | throw new DependencyNotResolvedError(this.name(), name); 114 | } 115 | 116 | return this._injections[name]; 117 | } 118 | 119 | initialize() { 120 | return this.initializeDependencies().then(() => 121 | super.initialize(this._settings) 122 | ); 123 | } 124 | 125 | connect() { 126 | return this.connectDependencies().then(() => super.connect()); 127 | } 128 | 129 | authenticate() { 130 | return this.authenticateDependencies().then(() => super.authenticate()); 131 | } 132 | 133 | initializeDependencies() { 134 | return _waitForDependencies.call(this, d => 135 | this.dependency(d) 136 | .manager() 137 | .initialize() 138 | ); 139 | } 140 | 141 | connectDependencies() { 142 | return _waitForDependencies.call(this, d => 143 | this.dependency(d) 144 | .manager() 145 | .connect() 146 | ); 147 | } 148 | 149 | authenticateDependencies() { 150 | return _waitForDependencies.call(this, d => 151 | this.dependency(d) 152 | .manager() 153 | .authenticate() 154 | ); 155 | } 156 | 157 | createService() { 158 | return { manager: () => this }; 159 | } 160 | } 161 | 162 | export { 163 | ServiceManager as default, 164 | InvalidServiceError, 165 | UnknownDependencyError, 166 | DependencyNotResolvedError 167 | }; 168 | -------------------------------------------------------------------------------- /src/eth/TransactionManager.js: -------------------------------------------------------------------------------- 1 | import PublicService from '../core/PublicService'; 2 | import TransactionObject from './TransactionObject'; 3 | import { Contract } from 'ethers'; 4 | import { dappHub } from '../../contracts/abis'; 5 | 6 | let txId = 1; 7 | 8 | export default class TransactionManager extends PublicService { 9 | constructor(name = 'transactionManager') { 10 | super(name, ['web3', 'log', 'nonce']); 11 | this._transactions = []; 12 | this._listeners = []; 13 | } 14 | 15 | formatHybridTx(contract, method, args, name, businessObject = null) { 16 | if (!args) args = []; 17 | let options, 18 | metadata = { 19 | contract: name, 20 | method: method.replace(/\(.*\)$/g, ''), 21 | args 22 | }, 23 | lastArg = args[args.length - 1]; 24 | 25 | if (typeof lastArg === 'object' && lastArg.constructor === Object) { 26 | options = lastArg; 27 | args = args.slice(0, args.length - 1); 28 | 29 | // append additional metadata to the default values. 30 | if (options.metadata) { 31 | metadata = { ...metadata, ...options.metadata }; 32 | delete options.metadata; 33 | } 34 | } else { 35 | options = {}; 36 | } 37 | 38 | return this.createHybridTx( 39 | // this async immediately-executed function wrapper is necessary to ensure 40 | // that the hybrid tx gets wrapped around the async operation that gets 41 | // the nonce. if we were to await outside of the wrapper, it would cause 42 | // `formatHybridTx` to return a promise that resolved to the hybrid, 43 | // instead of the hybrid itself, and then the hybrid's lifecycle hooks 44 | // wouldn't be accessible. 45 | (async () => 46 | this._execute(contract, method, args, { 47 | ...options, 48 | ...this.get('web3').transactionSettings(), 49 | nonce: await this.get('nonce').getNonce() 50 | }))(), 51 | { businessObject, metadata } 52 | ); 53 | } 54 | 55 | createHybridTx(tx, { businessObject, parseLogs, metadata } = {}) { 56 | if (tx._original) { 57 | console.warn('Redundant call to createHybridTx'); 58 | return tx; 59 | } 60 | 61 | const txo = new TransactionObject( 62 | tx, 63 | this.get('web3'), 64 | this.get('nonce'), 65 | businessObject, 66 | parseLogs 67 | ); 68 | 69 | const hybrid = txo.mine(); 70 | Object.assign( 71 | hybrid, 72 | { 73 | _original: txo, 74 | getOriginalTransaction: () => txo, 75 | _txId: txId++, 76 | metadata // put whatever you want in here for inspecting/debugging 77 | }, 78 | [ 79 | 'mine', 80 | 'confirm', 81 | 'state', 82 | 'isPending', 83 | 'isMined', 84 | 'isFinalized', 85 | 'isError', 86 | 'onPending', 87 | 'onMined', 88 | 'onFinalized', 89 | 'onError' 90 | ].reduce((acc, method) => { 91 | acc[method] = (...args) => txo[method](...args); 92 | return acc; 93 | }, {}) 94 | ); 95 | 96 | this._transactions.push(hybrid); 97 | this._listeners.forEach(cb => cb(hybrid)); 98 | 99 | return hybrid; 100 | } 101 | 102 | getTransactions() { 103 | return this._transactions; 104 | } 105 | 106 | onNewTransaction(callback) { 107 | this._listeners.push(callback); 108 | } 109 | 110 | // if options.dsProxyAddress is set, execute this contract method through the 111 | // proxy contract at that address. 112 | _execute(contract, method, args, options) { 113 | if (!options.dsProxyAddress) return contract[method](...args, options); 114 | 115 | const dsProxyAddress = options.dsProxyAddress; 116 | delete options.dsProxyAddress; 117 | 118 | this.get('log').debug(`Calling ${method} vis DSProxy at ${dsProxyAddress}`); 119 | const dsProxyContract = new Contract( 120 | dsProxyAddress, 121 | dappHub.dsProxy, 122 | this.get('web3') 123 | .ethersProvider() 124 | .getSigner() 125 | ); 126 | 127 | const data = contract.interface.functions[method](...args).data; 128 | return dsProxyContract.execute(contract.address, data, options); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/eth/TransactionObject.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | buildTestEthereumTokenService, 3 | buildTestService 4 | } from '../helpers/serviceBuilders'; 5 | import TestAccountProvider from '../helpers/TestAccountProvider'; 6 | import TransactionState from '../../src/eth/TransactionState'; 7 | import Web3Service from '../../src/eth/Web3Service'; 8 | import { promiseWait } from '../../src/utils'; 9 | import { ETH, WETH } from '../../src/eth/Currency'; 10 | 11 | let service; 12 | 13 | beforeAll(async () => { 14 | service = buildTestEthereumTokenService(); 15 | await service.manager().authenticate(); 16 | }); 17 | 18 | function createTestTransaction(srv = service) { 19 | const wethToken = srv.getToken(WETH); 20 | return wethToken.approveUnlimited(TestAccountProvider.nextAddress()); 21 | } 22 | 23 | test('event listeners work as promises', async () => { 24 | expect.assertions(3); 25 | const tx = createTestTransaction(); 26 | tx.onPending().then(tx => { 27 | expect(tx.state()).toBe(TransactionState.pending); 28 | }); 29 | 30 | tx.onMined().then(tx => { 31 | expect(tx.state()).toBe(TransactionState.mined); 32 | 33 | // create more blocks so that the original tx gets confirmed 34 | for (let i = 0; i < 3; i++) { 35 | createTestTransaction(); 36 | } 37 | }); 38 | 39 | tx.onFinalized().then(tx => { 40 | expect(tx.state()).toBe(TransactionState.finalized); 41 | }); 42 | 43 | await tx.confirm(); 44 | }); 45 | 46 | test('get fees', async () => { 47 | const tx = await createTestTransaction().mine(); 48 | expect(tx.fees().gt(ETH.wei(20000))).toBeTruthy(); 49 | }); 50 | 51 | test('event listeners work as callbacks', async () => { 52 | expect.assertions(3); 53 | const tx = createTestTransaction(); 54 | tx.onPending(() => { 55 | expect(tx.state()).toBe(TransactionState.pending); 56 | }); 57 | tx.onMined(() => { 58 | expect(tx.state()).toBe(TransactionState.mined); 59 | 60 | // create more blocks so that the original tx gets confirmed 61 | for (let i = 0; i < 3; i++) { 62 | createTestTransaction(); 63 | } 64 | }); 65 | tx.onFinalized(() => { 66 | expect(tx.state()).toBe(TransactionState.finalized); 67 | }); 68 | 69 | await tx.confirm(); 70 | }); 71 | 72 | class DelayingWeb3Service extends Web3Service { 73 | ethersProvider() { 74 | return new Proxy(super.ethersProvider(), { 75 | get(target, key) { 76 | if (key === 'getTransaction') { 77 | return async hash => { 78 | const tx = await target.getTransaction(hash); 79 | if (!tx) return; 80 | this._originalTx = tx; 81 | return { ...tx, blockHash: null }; 82 | }; 83 | } 84 | 85 | if (key === 'waitForTransaction') { 86 | return () => promiseWait(1000).then(() => this._originalTx); 87 | } 88 | 89 | return target[key]; 90 | } 91 | }); 92 | } 93 | } 94 | 95 | test('waitForTransaction', async () => { 96 | const service = buildTestService('token', { 97 | token: true, 98 | web3: [new DelayingWeb3Service(), { provider: { type: 'TEST' } }] 99 | }); 100 | await service.manager().authenticate(); 101 | 102 | const tx = createTestTransaction(service); 103 | await tx.mine(); 104 | expect(tx.state()).toBe('mined'); 105 | }); 106 | 107 | class FailingWeb3Service extends Web3Service { 108 | ethersProvider() { 109 | return new Proxy(super.ethersProvider(), { 110 | get(target, key) { 111 | if (key === 'getTransactionReceipt') { 112 | return async () => { 113 | throw new Error('test error'); 114 | }; 115 | } 116 | return target[key]; 117 | } 118 | }); 119 | } 120 | } 121 | 122 | test('error event listener works', async () => { 123 | expect.assertions(1); 124 | const service = buildTestService('token', { 125 | token: true, 126 | web3: [new FailingWeb3Service(), { provider: { type: 'TEST' } }] 127 | }); 128 | await service.manager().authenticate(); 129 | const tx = createTestTransaction(service); 130 | tx.onError(error => expect(error).toEqual('test error')); 131 | try { 132 | await tx; 133 | } catch (err) { 134 | // FIXME not sure why this try/catch is necessary... 135 | // console.log('hmm.', err); 136 | } 137 | }); 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dai.js 2 | 3 | [![node][node]][node-url] 4 | [![npm][npm]][npm-url] 5 | 9 | 10 | **Dai.js** is a JavaScript library that makes it easy to build applications on top of [MakerDAO][makerdao]'s Dai Stablecoin System. You can use Maker's contracts to open Collateralized Debt Positions, withdraw loans in Dai, trade tokens on OasisDEX, and more. 11 | 12 | The library features a pluggable, service-based architecture, which allows users maximal control when integrating the Maker functionality into existing infrastructures. It also includes convenient configuration presets for out-of-the-box usability, a powerful smart contract state inspector, and support for both front-end and back-end applications. 13 | 14 | Maker's entire suite of contracts will eventually be accessible through this library—including the DAO governance and the upcoming multi-collateral release—but functionality is limited in the current alpha version to the following areas: 15 | 16 | * Opening and shutting CDPs 17 | * Locking and unlocking collateral 18 | * Withdrawing and repaying Dai 19 | * Automated token conversions 20 | * Token contract functionality for WETH, PETH, MKR, Dai, and ETH 21 | * Buying and selling MKR and Dai with built-in DEX integration 22 | 23 | ## Usage 24 | 25 | Use NPM or Yarn to install the library: 26 | ``` 27 | npm install @makerdao/dai 28 | ``` 29 | 30 | Then include it: 31 | 32 | ```js 33 | import Maker from '@makerdao/dai'; 34 | // or: 35 | const Maker = require('@makerdao/dai'); 36 | ``` 37 | 38 | Example for transferring Dai: 39 | ```js 40 | import Maker from '@makerdao/dai'; 41 | const maker = Maker.create('test'); 42 | await maker.authenticate(); 43 | 44 | transferDai(address, amount) { 45 | const dai = maker.service('token').getToken(tokens.DAI); 46 | return dai.transfer(address, amount); 47 | } 48 | ``` 49 | 50 | Example for using CDPs: 51 | ```js 52 | import Maker from '@makerdao/dai'; 53 | const maker = Maker.create('test'); 54 | await maker.authenticate(); 55 | const cdp = await maker.openCdp(); 56 | const info = await cdp.getInfo(); 57 | console.log(info); 58 | ``` 59 | 60 | For full documentation, please refer to [https://makerdao.com/documentation/][docs]. 61 | 62 | For example code that consumes the library, check out [this repository](https://github.com/makerdao/integration-examples). 63 | 64 | ## Developing 65 | 66 | 1. `git clone https://github.com/makerdao/dai.js` 67 | 2. `yarn install` 68 | 69 | ### Running the tests 70 | 71 | The test suite is configured to run on a Ganache test chain. Before running the tests with `yarn test`, the test chain will start from a snapshot that has the Maker contracts deployed to it. 72 | 73 | If you want to re-run the tests whenever you make a change to the code, use `yarn test:watch`. 74 | 75 | If you want to start a test chain and leave it running, use `yarn test:net`. 76 | 77 | ### Handling changes to contract code 78 | 79 | If you have deployed contract code changes to the testchain, run `scripts/install-testchain-outputs.sh` to copy any updated ABI files and contract addresses to their expected locations. 80 | 81 | ### Commands 82 | 83 | - `yarn build:backend` - create backend build in `dist` folder 84 | - `yarn build:frontend` - create a UMD build in `dist` folder 85 | - `yarn lint` - run an ESLint check 86 | - `yarn coverage` - run code coverage and generate report in the `coverage` folder 87 | - `yarn test` - start a test chain and run all tests 88 | - `yarn test:watch` - start a test chain and run all tests in watch mode 89 | - `yarn test:net` - just start a test chain 90 | 91 | ## License 92 | 93 | **Dai.js** is available under the MIT license included with the code. 94 | 95 | [npm]: https://img.shields.io/badge/npm-5.6.0-blue.svg 96 | [npm-url]: https://npmjs.com/ 97 | 98 | [node]: https://img.shields.io/node/v/latest.svg 99 | [node-url]: https://nodejs.org 100 | 101 | [tests]: http://img.shields.io/travis/makerdao/dai.js.svg 102 | [tests-url]: https://travis-ci.org/makerdao/dai.js 103 | 104 | [cover]: https://codecov.io/gh/makerdao/dai.js/branch/master/graph/badge.svg 105 | [cover-url]: https://codecov.io/gh/makerdao/dai.js 106 | 107 | [makerdao]: https://makerdao.com 108 | [docs]: https://makerdao.com/documentation 109 | --------------------------------------------------------------------------------