NAV Navbar
javascript
  • Dai.js
  • Getting Started
  • Maker
  • CDP
  • Tokens
  • Price Service
  • ETH CDP Service
  • Token Conversion
  • Exchange Service
  • System Status
  • Events
  • Transactions
  • Using Multiple Accounts
  • Using DSProxy
  • Web3 Service
  • 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 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>
    Maker.create('http',{
        privateKey: YOUR_PRIVATE_KEY,
        url: 'https://kovan.infura.io/v3/YOUR_INFURA_PROJECT_ID'
    })
        .then(maker => { window.maker = maker })
        .then(() => maker.authenticate())
      .then(() => maker.openCdp())
      .then(cdp => console.log(cdp.id));
    </script>
    

    This library is also accessible as a UMD module.

    Quick Example

    import Maker from '@makerdao/dai';
    
    async function openLockDraw() {
        const maker = await Maker.create("http", {
            privateKey: YOUR_PRIVATE_KEY,
            url: 'https://kovan.infura.io/v3/YOUR_INFURA_PROJECT_ID'
        });
    
      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 = await Maker.create('browser');
    
    const makerHttp = await Maker.create('http', {
      privateKey: YOUR_PRIVATE_KEY,
      url: 'https://kovan.infura.io/v3/YOUR_INFURA_PROJECT_ID'
    });
    
    const makerTest = await 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 = await Maker.create('http', {
      privateKey: YOUR_PRIVATE_KEY, // '0xabc...'
      url: 'http://some-ethereum-rpc-node.net',
      provider: {
        type: 'HTTP', // or 'TEST'
        network: 'kovan'
      },
      web3: {
        statusTimerDelay: 2000,
        confirmedBlockCount: 8
        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 an options argument
    cdp.lockEth(0.25, { unit: ETH });
    cdp.lockEth(250000000000000000, { unit: 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.

    Properties

    .id

    This is the ID of the 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 debt 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.

    Note: this is often referred to as the Stability Fee, even though technically the Stability Fee is the fee that is paid in Dai, and the Governance Fee is the fee that is paid in MKR. But since fees are only paid in MKR in Single-Collateral Dai, and only paid in Dai in Multi-Collateral Dai, the fee in Single-Collateral Dai is often referred to as the Stability Fee to be consistent with the term that will be used in Multi-Collateral Dai and to avoid unduly confusing regular users.

    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.

    enoughMkrToWipe

    const enoughMkrToWipe = await cdp.enoughMkrToWipe(10000000000000000000, DAI.wei);
    

    cdp.enoughMkrToWipe(dai) returns true if the current account owns enough MKR to wipe the specified amount of Dai from the CDP.

    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.

    Note: this process is not atomic, so it's possible for some of the transactions to succeed but not all three. See Using DsProxy for executing multiple transactions atomically.

    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 to wipe all the dai debt and enough MKR to pay for all the accrued stability fee

    bite

    return await cdp.bite();
    

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

    Tokens

    const tokenService = maker.service('token');
    const dai = tokenService.getToken(DAI);
    const weth = tokenService.getToken(WETH);
    const peth = tokenService.getToken(PETH);
    

    Get a token object through the getToken(tokenSymbol) function on the tokenService

    Here's the list of tokens that can be passed into getToken(): DAI, MKR, WETH, PETH, ETH (this list can also be obtained with tokenService.getTokens(). This function returns a string representation of the token symbol, e.g. 'DAI', which can also be passed into getToken).

    The below methods can be called on any token object, except for deposit and withdraw are for Weth only, and join and exit are for Peth only.

    allowance

    const allowance = await dai.allowance('0x...owner', '0x...spender');
    

    allowance returns a currency unit representing the token allowance

    balance

    const balance = await dai.balance();
    

    balance returns a currency unit representing the token balance of the current account

    balanceOf

    const balanceOf = await dai.balanceOf('0x...f00');
    

    balanceOf returns a currency unit representing the token balance of the supplied account

    totalSupply

    const totalSupply = await dai.totalSupply();
    

    totalSupply returns a currency unit representing the total token supply

    approve

    return await dai.approve('0x...f00', DAI(10));
    

    approve approves the spending address to spend up to amount of msg.sender's tokens

    approveUnlimited

    return await dai.approveUnlimited('0x...f00');
    

    approveUnlimited approves the spending address to spend the maximum amount of msg.sender's tokens

    transfer

    return await dai.transfer('0x...f00', DAI(10));
    

    transfer transfers amount of token to to address

    transferFrom

    return await dai.transferFrom('0x...fr0m', '0x...t0', DAI(10));
    

    transferFrom() transfers amount of token from from address to to address. Transaction will fail if msg.sender does not have allowance to transfer the amount of tokens from the from address.

    deposit (WETH only)

    return await weth.deposit(ETH(10));
    

    deposit converts amount of Eth to amount of Weth

    withdraw (WETH only)

    return await weth.withdraw(WETH(10));
    

    withdraw converts amount of Weth to amount of Eth

    join (PETH only)

    return await peth.join(WETH(10));
    

    join converts amount of Weth to Peth, at the Weth to Peth Ratio

    exit (PETH only)

    return await peth.exit(PETH(10));
    

    withdraw converts amount of Peth to Weth, at the Weth to Peth Ratio

    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

    const pethPrice = await 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();
    

    getLiquidationRatio() returns a decimal representation of the liquidation ratio, 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 annual governance fee, e.g. 0.005.

    Note: this is often referred to as the Stability Fee, even though technically the Stability Fee is the fee that is paid in Dai, and the Governance Fee is the fee that is paid in MKR. But since fees are only paid in MKR in Single-Collateral Dai, and only paid in Dai in Multi-Collateral Dai, the fee in Single-Collateral Dai is often referred to as the Stability Fee to be consistent with the term that will be used in Multi-Collateral Dai and to avoid unduly confusing regular users.

    Token Conversion

    const conversionService = maker.service('conversion');
    

    Get the token conversion service with maker.service('conversion').

    The token conversion service offers functions to convert between Eth, Weth and Peth, handling allowances when necessary.

    convertEthToWeth

    return await conversionService.convertEthToWeth(ETH(10));
    

    Note: this is the same as weth.deposit

    convertEthToWeth deposits ETH into the WETH contract

    convertWethToPeth

    return await conversionService.convertWethToPeth(WETH(10));
    

    convertWethToPeth joins WETH into PETH, first giving token allowance if necessary

    Note: this process is not atomic if a token allowance needs to be set, so it's possible for one of the transactions to succeed but not both. See Using DsProxy for executing multiple transactions atomically. Also, peth.join can be called instead if you do not want the allowance to be set first automatically.

    convertEthToPeth

    return await conversionService.convertEthToPeth(ETH(10));
    

    convertEthToPeth awaits convertEthToWeth, then calls convertWethToPeth

    Note: this process is not atomic, so it's possible for some of the transactions to succeed but not all. See Using DsProxy for executing multiple transactions atomically.

    convertWethToEth

    return await conversionService.convertconvertWethToEth(WETH(10));
    

    convertWethToEth withdraws Eth from Weth contract

    Note: this is the same as weth.withdraw

    convertPethToWeth

    return await conversionService.convertPethToWeth(PETH(10));
    

    convertPethToWeth exits PETH into WETH, first giving token allowance if necessary

    Note: this process is not atomic if a token allowance needs to be set, so it's possible for one of the transactions to succeed but not both. See Using DsProxy for executing multiple transactions atomically. Also, peth.exit can be called instead if you do not want the allowance to be set first automatically.

    convertPethToEth

    return await conversionService.convertPethToEth(PETH(10));
    

    convertPethToEth awaits convertPethToWeth, then calls convertWethToEth

    Note: this process is not atomic, so it's possible for some of the transactions to succeed but not all. See Using DsProxy for executing multiple transactions atomically.

    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, which historically has been 1. It returns a USD_DAI price unit.

    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 txMgr = maker.service('transactionManager');
    // instance of transactionManager
    const open = maker.service('cdp').openCdp();
    // open is a transactionObject when promise resolves
    

    The transactionManager service is used to access and monitor TransactionObjects which can be used to track a transaction's status as it propagates through the blockchain. TransactionObjects have methods which trigger callbacks in the event of a transaction's status being pending, mined, confirmed or error.

    Listen

    txMgr.listen(open, {
      pending: tx => {
        // do something when tx is pending
      },
      mined: tx => {
        // do something when tx is mined
      },
      confirmed: tx => {
        // do something when tx is confirmed       
      },
      error: tx => {
        // do someting when tx fails
      }
    });
    
    await txMgr.confirm(open); // confirmed will fire after 5 blocks if default
    

    Accessing these status callbacks is done through the listen function from the transactionManager service. The listen function takes a promise and a callback object with the transaction status as a key.

    One caveat is that the confirmed event will not fire unless the transactionManager confirms the promise. This confirm() function waits on a number of blocks after the transaction has been mined, of which the default is 5, to resolve. To change this, the confirmedBlockCount attribute in the options object can be modified.

    Transaction Metadata

    const lock = cdp.lockEth(1);
    txMgr.listen(lock, {
      pending: tx => {
        const {contract, method} = tx.metadata;
        if(contract === 'WETH' && method === 'deposit') {
          console.log(tx.hash); // print hash for WETH.deposit
        }
      }
    })
    
    

    There are functions such as lockEth() which are composed of several internal transactions. These can be more accurately tracked by accessing the tx.metadata in the callback which contains both the contract and the method the internal transactions were created from.

    TransactionObject Methods

    A TransactionObject also has a few methods to provide further details on the transaction:

    Using Multiple Accounts

    const maker = await 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"
    
    await maker.addAccount({type: privateKey, key: fourthAccount.key}); // the name argument is optional
    maker.useAccountWithAddress(fourthAccount.address)
    const cdp4 = await maker.openCdp(); //owned by the fourth account
    

    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, or useAccountWithAddress to switch to using an account by its address, 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 = await 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 = await 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.

    Using DSProxy

    The DSProxyService includes all the functionality necessary for interacting with both types of proxies found in Maker products: profile proxies and forwarding proxies.

    Forwarding proxies are simple contracts that aggregate function calls in the body of a single method. These are used in the CDP Portal and Oasis Direct in order to allow users to execute multiple transactions atomically, which is both safer and more user-friendly than implementing several steps as discrete transactions.

    // Forwarding proxy
    
    function lockAndDraw(address tub_, bytes32 cup, uint wad) public payable {
      lock(tub_, cup);
      draw(tub_, cup, wad);
    }
    

    Forwarding proxies are meant to be as simple as possible, so they lack some features that could be important if they are to be used as interfaces for more complex smart contract logic. This problem can be solved by using profile proxies (i.e., copies of DSProxy) to execute the functionality defined in the forwarding proxies.

    The first time an account is used to interact with any Maker application, the user will be prompted to deploy a profile proxy. This copy of DSProxy can be used in any product, including dai.js, by way of a universal proxy registry. Then, the calldata from any function in the forwarding proxy can be passed to DSProxy's execute() method, which runs the provided code in the context of the profile proxy.

    // Calling the forwarding proxy with dai.js
    
    function lockAndDraw(tubContractAddress, cdpId, daiAmount, ethAmount) {
      const saiProxy = maker.service('smartContract').getContractByName('SAI_PROXY');
    
      return saiProxy.lockAndDraw(
        tubContractAddress,
        cdpId,
        daiAmount,
        {
          value: ethAmount,
          dsProxy: true
        }
      );
    }
    
    

    This makes it possible for users' token allowances to persist from one Maker application to another, and it allows users to recover any funds mistakenly sent to the proxy's address.

    Many of the functions in DSProxyService will only be relevant to power users. All that is strictly required to automatically generate a function's calldata and find the correct profile proxy is the inclusion of { dsProxy: true } in the options object for any transaction — provided the user has already deployed a profile proxy. If that's not certain, it may also be necessary to query the registry to determine if a user already owns a proxy, and to build one if they do not.

    currentProxy

    asnyc function getProxy() {
      return maker.service('proxy').currentProxy();
    }
    

    If the currentAccount (according the Web3Service) has already deployed a DSProxy, currentProxy() returns its address. If not, it returns null. It will update automatically in the event that the active account is changed.

    This function should be used to check whether a user has a proxy before attempting to build one.

    build

    async function buildProxy() {
      const proxyService = maker.service('proxy');
      if (!proxyService.currentProxy()) {
        return await proxyService.build();
      }
    }
    

    build will deploy a copy of DSProxy owned by the current account. This transaction will revert if the current account already owns a profile proxy.

    By default, build() returns after the transaction is mined.

    execute

    function lockAndDraw(tubContractAddress, cdpId, daiAmount, ethAmount) {
      const saiProxy = maker.service('smartContract').getContractByName('SAI_PROXY');
    
      return saiProxy.lockAndDraw(
        tubContractAddress,
        cdpId,
        daiAmount,
        {
          value: ethAmount,
          dsProxy: true
        }
      );
    }
    
    

    This function is designed to be called by the TransactionManager, not accessed directly. As long as the target contract is defined by the SmartContractService, the inclusion of the dsProxy key in the options for any transaction will automatically forward the transaction to the DSProxyService.

    The value of dsProxy can either be true or an explicitly provided DSProxy address.

    getProxyAddress

    const proxy = await maker.service('proxy').getProxyAddress('0x...');
    

    getProxyAddress will query the proxy registry for the profile proxy address associated with a given account. If no address is provided as a parameter, the function will return the address of the proxy owned by the currentAccount.

    getOwner

    const owner = await maker.service('proxy').getOwner('0x...');
    

    getOwner will query the proxy registry for the owner of a provided instance of DSProxy.

    setOwner

    async function giveProxy(newOwner, proxyAddress) {
      return await maker.service('proxy').setOwner(newOwner, proxyAddress);
    }
    

    setOwner can be used to give a profile proxy to a new owner. The address of the recipient account must be specified, but the DSProxy address will default to currentProxy if the second parameter is excluded.

    Web3 Service

    const web3Service = maker.service('web3');
    

    The web3 service manages dai.js' connection with the blockchain

    disconnect

    web3Service.disconnect();
    

    disconnect closes the connection with the blockchain.

    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 = await 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 = await Maker.create('http', {web3: "MyCustomWeb3Service"});
      const mycustomWeb3Service = maker.service('web3');
    });
    
    //step 10: in ExampleService.spec.js
    const maker = await 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 = await 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.