NAV Navbar
javascript
  • Dai.js
  • Getting Started
  • Maker
  • CDP
  • Price Service
  • ETH CDP Service
  • Exchange Service
  • System Status
  • Events
  • Transactions
  • Using Multiple Accounts
  • Developing
  • Adding a New Service
  • Dai.js

    tests coverage

    Dai.js is a JavaScript library that makes it easy to build applications on top of MakerDAO's platform of smart contracts. You can use Maker's contracts to open Collateralized Debt Positions (CDPs), withdraw loans in Dai, trade tokens on OasisDEX, and more.

    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.

    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:

    For example code that consumes the library, check out this repository.

    Getting Started

    Installation

    import Maker from '@makerdao/dai';
    // or:
    const Maker = require('@makerdao/dai');
    

    Install the package with npm:

    npm install @makerdao/dai

    Once it's installed, import the module into your project as shown on the right.

    UMD

    <script src="./dai.js" />
    
    <script>
    var maker = Maker.create('kovan', { privateKey: YOUR_PRIVATE_KEY });
    
    maker.authenticate()
      .then(() => maker.openCdp())
      .then(cdp => cdp.getId())
      .then(id => console.log(id));
    </script>
    

    This library is also accessible as a UMD module.

    Quick Example

    import Maker from '@makerdao/dai';
    
    const maker = Maker.create("kovan", { privateKey: YOUR_PRIVATE_KEY });
    
    async function openLockDraw() {
      await maker.authenticate();
      const cdp = await maker.openCdp();
    
      await cdp.lockEth(0.25);
      await cdp.drawDai(50);
    
      const debt = await cdp.getDebtValue();
      console.log(debt.toString); // '50.00 DAI'
    }
    
    openLockDraw();
    

    Using the Maker and Cdp APIs, the code shown on the right opens a cdp, locks 0.25 ETH into it, draws out 50 Dai, and then logs information about the newly opened position to the console.

    Maker

    Presets

    const makerBrowser = Maker.create('browser');
    
    const makerHttp = Maker.create('http', {
      privateKey: YOUR_PRIVATE_KEY,
      url: 'https://sai-service.makerdao.com/node'
    });
    
    const makerKovan = Maker.create('kovan', {
      privateKey: YOUR_PRIVATE_KEY,
      provider: {
        infuraApiKey: YOUR_INFURA_API_KEY
      }
    });
    
    const makerTest = Maker.create('test');
    

    When instantiating a Maker object, you pass in the name of a configuration preset and an options hash.

    Available presets are listed below, with the required options for each shown on the right.

    Options

    const maker = Maker.create('http', {
      privateKey: YOUR_PRIVATE_KEY, // '0xabc...'
      url: 'https://sai-service.makerdao.com/node',
      provider: {
        type: 'HTTP', // 'INFURA', 'TEST'
        network: 'kovan',
        infuraApiKey: YOUR_INFURA_API_KEY
      },
      web3: {
        statusTimerDelay: 2000,
        transactionSettings: {
          gasPrice: 12000000000
        }
      }
      log: false
    });
    

    authenticate

    await maker.authenticate();
    

    After creating your Maker instance, and before using any other methods, run this. It makes sure all services are initialized, connected to any remote API's, and properly authenticated.

    service

    const priceService = maker.service('price');
    

    The service function can be used to access services that were injected into your instance of maker. See the service documentation sections below.

    openCdp

    const newCdp = await maker.openCdp();
    

    maker.openCdp() will create a new CDP, and then return the CDP object, which can be used to access other CDP functionality.

    The promise will resolve when the transaction is mined.

    getCdp

    const cdp = await maker.getCdp(614);
    

    maker.getCdp(id) creates a CDP object for an existing CDP. The CDP object can then be used to interact with your CDP.

    Units

    import Maker from '@makerdao/dai';
    const {
      MKR,
      DAI,
      ETH,
      WETH,
      PETH,
      USD_ETH,
      USD_MKR,
      USD_DAI
    } = Maker;
    
    // These are all identical:
    
    // each method has a default type
    cdp.lockEth(0.25);
    cdp.lockEth('0.25');
    
    // you can pass in a currency unit instance
    cdp.lockEth(ETH(0.25));
    cdp.lockEth(ETH.wei(250000000000000000));
    
    // you can pass the unit as a second argument
    cdp.lockEth(0.25, ETH);
    cdp.lockEth(250000000000000000, ETH.wei);
    
    const eth = ETH(5);
    eth.toString() == '5.00 ETH';
    
    const price = USD_ETH(500);
    price.toString() == '500.00 USD/ETH';
    
    // multiplication handles units
    const usd = eth.times(price);
    usd.toString() == '2500.00 USD';
    
    // division does too
    const eth2 = usd.div(eth);
    eth2.isEqual(eth);
    

    Methods that take numerical values as input can also take instances of token classes that the library provides. These are useful for managing precision, keeping track of units, and passing in wei values.

    Most methods that return numerical values return them wrapped in one of these classes.

    There are two types:

    The classes that begin with USD are price units; e.g. USD_ETH represents the price of ETH in USD.

    Useful instance methods:

    CDP

    Now that you've used maker to either open a new CDP or retrieve an existing one, you can use the returned cdp object to call functions on it.

    getId

    const id = await cdp.getId();
    

    Use cdp.getId() to retrieve the ID from a CDP object. You can pass this ID to Maker.getCdp.

    getDebtValue

    const daiDebt = await cdp.getDebtValue();
    const usdDebt = await cdp.getDebtValue(Maker.USD);
    

    cdp.getDebtValue() returns the amount of that has been borrowed against the collateral in the CDP. By default it returns the amount of Dai as a currency unit, but can return the equivalent in USD if the first argument is Maker.USD.

    getGovernanceFee

    const mkrFee = await cdp.getGovernanceFee();
    const usdFee = await cdp.getGovernanceFee(Maker.USD);
    

    cdp.getGovernanceFee() returns the value of the accrued governance fee. By default it returns the amount of MKR as a currency unit, but can return the equivalent in USD if the first argument is Maker.USD.

    getCollateralizationRatio

    const ratio = await cdp.getCollateralizationRatio();
    

    cdp.getCollateralizationRatio() returns the USD value of the collateral in the CDP divided by the USD value of the Dai debt for the CDP, e.g. 2.5.

    getLiquidationPrice

    const ratio = await cdp.getLiquidationPrice();
    

    cdp.getLiquidationPrice() returns the price of Ether in USD that causes the CDP to become unsafe (able to be liquidated), all other factors constant. It returns a USD_ETH price unit.

    getCollateralValue

    const ethCollateral = await cdp.getCollateralValue();
    const pethCollateral = await cdp.getCollateralValue(Maker.PETH);
    const usdCollateral = await cdp.getCollateralValue(Maker.USD);
    

    cdp.getCollateralValue() returns the value of the collateral in the CDP. By default it returns the amount of ETH as a currency unit, but can return the equivalent in PETH or USD depending on the first argument.

    isSafe

    const ratio = await cdp.isSafe();
    

    cdp.isSafe() returns true if the cdp is safe, that is, if the USD value of its collateral is greater than or equal to USD value of the its debt multiplied by the liquidation ratio.

    lockEth

    return await cdp.lockEth(10000000000000000000, ETH.wei);
    // or equivalently
    return await cdp.lockEth(100, ETH);
    

    cdp.lockEth(eth) abstracts the token conversions needed to lock collateral in a CDP. It first converts the ETH to WETH, then converts the WETH to PETH, then locks the PETH in the CDP.

    drawDai

    return await cdp.drawDai(10000000000000000000, DAI.wei);
    // or equivalently
    return await cdp.drawDai(100, DAI);
    

    cdp.drawDai(dai) withdraws the specified amount of Dai as a loan against the collateral in the CDP. As such, it will fail if the CDP doesn't have enough PETH locked in it to remain at least 150% collateralized.

    wipeDai

    return await cdp.wipeDai(10000000000000000000, DAI.wei);
    // or equivalently
    return await cdp.wipeDai(100, DAI);
    

    cdp.wipeDai(dai) sends Dai back to the CDP in order to repay some (or all) of its outstanding debt.

    Note: CDPs accumulate MKR governance debt over their lifetime. This must be paid when wiping dai debt, and thus MKR must be acquired before calling this method.

    freePeth

    return await cdp.freePeth(100, PETH);
    // or equivalently
    return await cdp.freePeth(10000000000000000000, PETH.wei);
    

    cdp.freePeth(peth) withdraws the specified amount of PETH and returns it to the owner's address. As such, the contract will only allow you to free PETH that's locked in excess of 150% of the CDP's outstanding debt.

    give

    return await cdp.give('0x046ce6b8ecb159645d3a605051ee37ba93b6efcc');
    

    cdp.give(address) transfers ownership of the CDP from the current owner to the address you provide as an argument.

    shut

    return await cdp.shut();
    

    cdp.shut() wipes all remaining dai, frees all remaining collateral, and deletes the CDP. This will fail if the caller does not have enough DAI and MKR to wipe all debt.

    bite

    return await cdp.bite();
    

    cdp.bite() will initiate the liquidation process of an undercollateralized CDP

    Price Service

    const price = maker.service('price');
    

    Retrieve the PriceService through Maker.service('price').

    The PriceService exposes the collateral and governance tokens' price information that is reported by the oracles in the Maker system.

    getEthPrice

    const ethPrice = await price.getEthPrice();
    

    Get the current USD price of ETH, as a USD_ETH price unit.

    getMkrPrice

    const mkrPrice = await price.getMkrPrice();
    

    Get the current USD price of the governance token MKR, as a USD_MKR price unit.

    getPethPrice

    await pethPrice = price.getPethPrice();
    

    Get the current USD price of PETH (pooled ethereum), as a USD_PETH price unit.

    setEthPrice

    await price.setEthPrice(475);
    

    Set the current USD price of ETH.

    This requires the necessary permissions and will only be useful in a testing environment.

    setMkrPrice

    await price.setMkrPrice(950.00);
    

    Set the current USD price of the governance token MKR.

    This requires the necessary permissions and will only be useful in a testing environment.

    getWethToPethRatio

    await price.getWethToPethRatio();
    

    Returns the current W-ETH to PETH ratio.

    ETH CDP Service

    const ethCdp = maker.service('cdp');
    

    Retrieve the ETH CDP Service through Maker.service('cdp').

    The ETH CDP Service exposes the risk parameter information for the Ether CDP type (in single-collateral Dai, this is the only CDP Type)

    getLiquidationRatio

    const ratio = await ethCdp.getLiquidationRatio();
    

    getLiquidationPenalty() returns a decimal representation of the liquidation penalty, e.g. 1.5

    getLiquidationPenalty

    const penalty = await ethCdp.getLiquidationPenalty();
    

    getLiquidationPenalty() returns a decimal representation of the liquidation penalty, e.g. 0.13

    getAnnualGovernanceFee

    const fee = await ethCdp.getAnnualGovernanceFee();
    

    getAnnualGovernanceFee() returns a decimal representation of the liquidation penalty, e.g. 0.005

    Exchange Service

    const exchange = maker.service('exchange');
    

    Retrieve the OasisExchangeService (or alternative implementation) through Maker.service('exchange').

    The exchange service allows to buy and sell DAI, MKR, and other tokens. The default OasisExchangeService implementation uses the OasisDEX OTC market for this.

    sellDai

    // Sell 100.00 DAI for 0.30 WETH or more.
    const sellOrder = await exchange.sellDai('100.0', 'WETH', '0.30');
    

    Sell a set amount of DAI and receive another token in return.

    buyDai

    // Buy 100.00 DAI for 0.30 WETH or less.
    const buyOrder = await exchange.buyDai('100.0', 'WETH', '0.35');
    

    Buy a set amount of DAI and give another token in return.

    OasisOrder

    const buyOrder = await exchange.buyDai('100.0', 'WETH', '0.35');
    const fillAmount = buyOrder.fillAmount();
    const gasPaid = buyOrder.fees();
    const created = buyOrder.created();
    

    OasisOrders have a few methods: * fillAmount: amount of token received in exchange * fees(): amount of ether spent on gas * created(): timestamp of when transaction was mined

    System Status

    const ethCdp = maker.service('cdp');
    

    To access system status information, retrieve the ETH CDP Service through Maker.service('cdp').

    getSystemCollateralization

    const systemRatio = await ethCdp.getSystemCollateralization();
    

    getSystemCollateralization() returns the collateralization ratio for the entire system, e.g. 2.75

    getTargetPrice

    const targetPrice = await ethCdp.getTargetPrice();
    

    getTargetPrice() returns the target price of Dai in USD, that is, the value to which Dai is soft-pegged, e.g. 1.0

    Events

    The event pipeline allows developers to easily create real-time applications by letting them listen for important state changes and lifecycle events.

    Wildcards

    An event name passed to any event emitter method can contain a wildcard (the * character). A wildcard may appear as foo/*, foo/bar/*, or simply *.

    * matches one sub-level.

    e.g. price/* will trigger on both price/USD_ETH and price/MKR_USD but not price/MKR_USD/foo.

    ** matches all sub-levels.

    e.g. price/** will trigger on price/USD_ETH, price/MKR_USD, and price/MKR_USD/foo.

    Event Object

    {
        type: <event_type>,
        payload: <event_payload>, /* if applicable */
        index: <event_sequence_number>,
        block: <latest_block_when_emitted>
    }
    

    Triggered events will receive the object shown on the right.

    Maker Object

    Price

    maker.on('price/ETH_USD', eventObj => {
        const { price } = eventObj.payload;
        console.log('ETH price changed to', price);
    })
    
    Event Name Payload
    price/ETH_USD { price }
    price/MKR_USD { price }
    price/WETH_PETH { ratio }

    Web3

    maker.on('web3/AUTHENTICATED', eventObj => {
        const { account } = eventObj.payload;
        console.log('web3 authenticated with account', account);
    })
    
    Event Name Payload
    web3/INITIALIZED { provider: { type, url } }
    web3/CONNECTED { api, network, node }
    web3/AUTHENTICATED { account }
    web3/DEAUTHENTICATED { }
    web3/DISCONNECTED { }

    CDP Object

    cdp.on('DEBT', eventObj => {
        const { dai } = eventObj.payload;
        console.log('Your cdp now has a dai debt of', dai);
    })
    
    Event Name Payload
    COLLATERAL { USD, ETH }
    DEBT { dai }

    Transactions

    TransactionObject Lifecycle Hooks

    const txObject = await cdp.wipeDai(1);
    // the transaction has been mined
    
    txObject.onPending(() => console.log('pending!'));
    txObject.onMined(() => console.log('mined!'));
    txObject.onFinalized(() => console.log('finalized!'));
    txObject.onError(() => console.log('error'));
    
    const pending = txObject.isPending();
    const mined = txObject.isPending();
    const finalized = txObject.isFinalized();
    const error = txObject.isError();
    
    // if you are using the onFinalized callback, you have to
    // call this method at some point.
    await txObject.confirm(5);
    // 5 = the number of blocks to wait; the default is 3
    
    // now there are five new blocks after the transaction's block
    // and the onFinalized callback has been called
    

    TransactionObjects have methods which can be used to trigger callbacks when the transaction is pending, or after a certain number of new blocks have been added after the block containing the transaction (when the transaction is "finalized").

    TransactionObject Transaction Data

    const txObject = await cdp.drawDai(1);
    const hash = txObject.hash();
    const fees = txObject.fees();
    const timeStamp = txObject.timeStamp();
    const timeStampSubmitted = txObject.timeStampSubmitted();
    

    TransactionObjects also have a few methods to provide details on the transaction: * hash: transaction hash * fees(): amount of ether spent on gas * timeStamp(): timestamp of when transaction was mined * timeStampSubmitted(): timestamp of when transaction was submitted to the network

    onNewTransaction

    maker.service('transactionManager').onNewTransaction(tx => {
      tx.onPending(() => {
        console.log(`${tx.metadata.contract}.${tx.metadata.method}: pending`);
      });
    });
    

    The onNewTransaction function in the transactionManager service allows you to register a callback that gets called every time a transaction is initiated by the dai.js library.

    Using Multiple Accounts

    const maker = Maker.create({
      url: 'http://localhost:2000',
      accounts: {
        other: {type: privateKey, key: someOtherKey},
        default: {type: privateKey, key: myKey}
      }
    });
    
    await maker.authenticate();
    await maker.addAccount('yetAnother', {type: privateKey, key: thirdKey});
    
    const cdp1 = await maker.openCdp(); // owned by "default"
    
    maker.useAccount('other');
    const cdp2 = await maker.openCdp(); // owned by "other"
    
    maker.useAccount('yetAnother');
    const cdp3 = await maker.openCdp(); // owned by "yetAnother"
    

    The library supports the use of multiple accounts (i.e. private keys) within a single Maker instance. Accounts can be specified in the constructor options or with the addAccount method.

    Call useAccount to switch to using an account by its name, and subsequent calls will use that account as the transaction signer.

    When the Maker instance is first created, it will use the account named default if it exists, or the first account in the list otherwise.

    Account types

    const maker = Maker.create({
      url: 'http://localhost:2000',
      accounts: {
        // this will be the first account from the provider at
        // localhost:2000
        first: {type: 'provider'},
    
        // this will be the current account in MetaMask, and it
        // will send its transactions through MetaMask
        second: {type: 'browser'},
    
        // this account will send its transactions through the
        // provider at localhost:2000
        third: {type: 'privateKey', key: myPrivateKey}
      }
    })
    

    In addition to the privateKey account type, there are two other built-in types:

    Plugins

    import TrezorPlugin from '@makerdao/dai-plugin-trezor-web';
    import LedgerPlugin from '@makerdao/dai-plugin-ledger-web';
    
    const maker = Maker.create({
      plugins: [
        TrezorPlugin,
        LedgerPlugin,
      ],
      accounts: {
        // default derivation path is "44'/60'/0'/0/0"
        myTrezor: {type: 'trezor', path: derivationPath1},
        myLedger: {type: 'ledger', path: derivationPath2}
      }
    });
    

    Plugins can add additional account types. There are currently two such plugins for hardware wallet support:

    Demo

    Install the multiple accounts demo app to see this functionality in action.

    Developing

    1. git clone https://github.com/makerdao/makerdao-integration-poc
    2. npm install

    Running the tests

    The test suite is configured to run on a Ganache test chain.

    If you run the tests with npm test, it will start a test chain and deploy all the smart contracts for the Dai stablecoin system. This takes a few minutes.

    To keep the test chain running and re-run the tests when changes are made to the code, use the command npm run test:watch.

    If you want to deploy the contracts to a test chain without running the test suite, use npm run test:net.

    Inspect contract state

    Start the dev server using npm start, then open http://localhost:9000.

    Commands

    Adding a New Service

    Note: This section is only for advanced users that are willing to modify the dai.js source code. In the future, you will be able to add custom services without needing to modify the source code.

    You can take advantage of the pluggable architecture of this library by choosing different implementations for services, and/or adding new service roles altogether. A service is just a Javascript class that inherits from either PublicService, PrivateService, or LocalService, and contains public method(s). It can depend on other services through a built-in dependency injection framework, and can also be configured through the Maker config file / config options.

    Steps to add a new service

    Here are the steps to add a new service called ExampleService to MakerJS:

    //example code in ExampleService.js for steps 3-6
    import PublicService from '../core/PublicService';
    
    export default class ExampleService extends PublicService {
        constructor (name='example') {
            super(name, ['log']);
        }
    
        test(){
            this.get('log').info('test');
        }
    
    //example code in ExampleService.spec.js for step 8
    import Maker from '../../src/index';
    
    //step 8: a new service role ('example') is used
    test('test 1', async () => {
      const maker = Maker.create('http', {example: "ExampleService"});
      const exampleService = customMaker.service('example');
      exampleService.test(); //logs "test"
    });
    
    //step 8: a custom service replaces a default service (Web3)
    test('test 2', async () => {
      const maker = Maker.create('http', {web3: "MyCustomWeb3Service"});
      const mycustomWeb3Service = maker.service('web3');
    });
    
    //step 10: in ExampleService.spec.js
    const maker = Maker.create('http', {example: ["ExampleService", {
        exampleSetting: "this is a configuration setting"
      }]});
    
    //step 10: accessing configuration settings in ExampleService.js
    initialize(settings){
      if(settings.exampleSetting){
        this.get('log').info(settings.exampleSetting);
      }
    }
    

    Service Lifecycle

    The three kinds of services mentioned in step 4 above follow the following state machine diagrams in the picture below.

    //example initialize() function in ExampleService.js
      initialize(settings) {
        this.get('log').info('ExampleService is initializing...');
        this._setSettings(settings);
      }
    

    To specify what initializing, connecting and authenticating entails, implement the initialize(), connect(), and authenticate() functions in the service itself. This will be called while the service's manager brings the service to the corresponding state.

    alt text

    const maker = Maker.create('http', {example: "ExampleService"});
    const exampleService = customMaker.service('example');
    
    //wait for example service and its dependencies to initialize
    await exampleService.manager().initialize();
    
    //wait for example service and its dependencies to connect
    await exampleService.manager().connect();
    
    //wait for example service and its dependencies to authenticate
    await exampleService.manager().authenticate();
    
    //can also use callback syntax
    exampleService.manager().onConnected(()=>{
        /*executed after connected*/
    });
    
    //wait for all services used by the maker object to authenticate
    maker.authenticate();
    

    A service will not finish initializing/connecting/authenticating until all of its dependent services have completed the same state (if applicable - for example a LocalService is considered authenticated/connected in addition to initialized, if it has finished initializing). The example code here shows how to wait for the service to be in a certain state.

    Adding Custom Events

    //in PriceService.js
    this.get('event').registerPollEvents({
          'price/ETH_USD': {
            price: () => this.getEthPrice()
          }
        });
    

    One way to add an event is to “register” a function that gets called on each new block, using the event service's registerPollEvents() function. For example, here is some code from the price service. this.getEthPrice() will be called on each new block, and if the state has changed from the last call, a price/ETH_USD event will be emitted with the payload { price: [new_price] }.

    //in Web3Service.js
    this.get('event').emit('web3/INITIALIZED', {
      provider: { ...settings.provider }
    });
    

    Another way to an add an event is to manually emit an event using the event service's emit function. For example, when the Web3Service initializes, it emits an event that contains info about the provider.

    //in the constructor in the Cdp.js
    this._emitterInstance = this._cdpService.get('event').buildEmitter();
    this.on = this._emitterInstance.on;
    this._emitterInstance.registerPollEvents({
      COLLATERAL: {
        USD: () => this.getCollateralValueInUSD(),
        ETH: () => this.getCollateralValueInEth()
      },
      DEBT: {
        dai: () => this.getDebtValueInDai()
      }
    });
    

    Note that calling registerPollEvents and emit() directly on the event service like in the previous two examples will register events on the "default" event emitter instance. However, you can create a new event emitter instance for your new service. For example, the CDP object defines it's own event emitter, as can be seen here, by calling the event service's buildEmitter() function.