├── .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 |
--------------------------------------------------------------------------------