All pages
Powered by GitBook
1 of 6

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Advanced

To override the address of one of the contracts used by Dai.js or a plugin, you can pass the smartContract.addressOverrides option. You need to know the key of the contract in the addresses file to override it.

List of mainnet addresses

const service = Maker.create('test' {
  smartContract: {
    addressOverrides: {
      PROXY_REGISTRY: '0xYourAddress'
    }
  } 
});

Transaction manager

The transactionManager service is used to track a transaction's status as it propagates through the blockchain.

Methods in Dai.js that start transactions are asynchronous, so they return promises. These promises can be passed as arguments to the transaction manager to set up callbacks when transactions change their status to pending, mined, confirmed or error.

Pass the promise to transactionManager.listen with callbacks, as shown below.

Note that the confirmed event will not fire unless transactionManager.confirm

is called. This async function waits a number of blocks (default 5) after the transaction has been mined to resolve. To change this globally, set the
confirmedBlockCount
attribute in Maker
. To change it for just one call, pass the number of blocks to wait as the second argument:

Transaction Metadata

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

Transaction Object Methods

A TransactionObject also has a few methods to provide further 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

options
const txMgr = maker.service('transactionManager');
// instance of transactionManager
const open = maker.service('cdp').openCdp();
// open is a promise--note the absence of `await`
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' callback will fire after 5 blocks
await txMgr.confirm(open, 3);
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
    }
  }
})

Adding a new service

Summary

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 our built-in dependency injection framework, and can also be configured through the Maker config file / config options.

Steps to add a new service

The information below is written for developers who want to add to dai.js, but you can also add services through the use of .

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

(1) In the src directory, create an ExampleService.js file in one of the subdirectories.

(2) In src/config/DefaultServiceProvider.js, import ExampleService.js and add it to the _services array

(3) Create a class called ExampleService in ExampleService.js

(4) The service must extend one of:

  • PrivateService - requires both a network connection and authentication

  • PublicService - requires just a network connection

  • LocalService - requires neither

See the Service Lifecycle section below for more info

(5) In the constructor, call the parent class's constructor with the following arguments:

  • The name of the service. This is how the service can be referenced by other services

  • An array of the names of services to depend on

(6) Add the necessary public methods

(7) If your service will be used to replace a default service (the full list of default service roles can be found in src/config/ConfigFactory), then skip this step. Otherwise, you'll need to add your new service role (e.g. "example") to the ServiceRoles array in src/config/ConfigFactory.

(8) Create a corresponding ExampleService.spec.js file in the test directory. Write a test in the test file that creates a Maker object using your service.

(9) (Optional) Implement the relevant service lifecycle functions (initialize(), connect(), and authenticate()). See the Service Lifecycle section below for more info

(10) (Optional) Allow for configuration. Service-specific settings can be passed into a service by the Maker config file or config options. These service-specific settings can then be accessed from inside a service as the parameter passed into the initialize function (see the Service Lifecycle section below)

Service Lifecycle

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

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.

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

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] }.

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.

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.

Events

Summary

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

Triggered events will receive the object shown on the right.

  • <event_type> - the name of the event

  • <event_payload> - the new state data sent with the event

  • <event_sequence_number> - a sequentially increasing index

Maker Object

Price

Web3

CDP Object

plugins
<latest_block_when_emitted> - the current block at the time of the emit

Event Name

Payload

price/ETH_USD

{ price }

price/MKR_USD

{ price }

price/WETH_PETH

{ ratio }

Event Name

Payload

web3/INITIALIZED

{ provider: { type, url } }

web3/CONNECTED

{ api, network, node }

web3/AUTHENTICATED

{ account }

web3/DEAUTHENTICATED

{ }

web3/DISCONNECTED

{ }

Event Name

Payload

COLLATERAL

{ USD, ETH }

DEBT

{ dai }

//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);
  }
}
//example initialize() function in ExampleService.js
  initialize(settings) {
    this.get('log').info('ExampleService is initializing...');
    this._setSettings(settings);
  }
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();
//in PriceService.js
this.get('event').registerPollEvents({
      'price/ETH_USD': {
        price: () => this.getEthPrice()
      }
    });

//in Web3Service.js
this.get('event').emit('web3/INITIALIZED', {
  provider: { ...settings.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()
  }
});
{
    type: <event_type>,
    payload: <event_payload>, /* if applicable */
    index: <event_sequence_number>,
    block: <latest_block_when_emitted>
}
maker.on('price/ETH_USD', eventObj => {
    const { price } = eventObj.payload;
    console.log('ETH price changed to', price);
})
maker.on('web3/AUTHENTICATED', eventObj => {
    const { account } = eventObj.payload;
    console.log('web3 authenticated with account', account);
})
cdp.on('DEBT', eventObj => {
    const { dai } = eventObj.payload;
    console.log('Your cdp now has a dai debt of', dai);
})

Using multiple accounts

Summary

Dai.js supports the use of multiple accounts (i.e. private keys) with a single Maker instance. Accounts can be specified in the options for Maker.create 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.

You can check the current account with currentAccount and currentAddress:

Account types

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

  • provider: Get the first account from the provider (e.g. the value from getAccounts).

  • browser: Get the first account from the provider in the browser (e.g. MetaMask), even if the Maker instance is configured to use a different provider.

Hardware wallets

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

Demo

Install the to see this functionality in action.

dai-plugin-trezor-web
dai-plugin-ledger-web
multiple accounts demo app
const maker = await Maker.create({
  url: 'http://localhost:2000',
  accounts: {
    other: {type: privateKey, key: someOtherKey},
    default: {type: privateKey, key: myKey}
  }
});

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
> maker.currentAccount()
{ name: 'other', type: 'privateKey', address: '0xfff...' }
> maker.currentAddress()
'0xfff...'
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}
  }
})
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}
  }
});

DSProxy

Summary

The DSProxyService includes all the functionality necessary for interacting with both types of proxy contracts used 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 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 . 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.

This makes it possible for users' token allowances to persist from one Maker application to another, and it allows users to 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()

  • Params: None

  • Returns: promise (resolves to address or null)

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()

  • Params: None

  • Returns: TransactionObject

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.

ensureProxy()

This convenience function will either return an existing proxy or create one.

getProxyAddress()

  • Params: Address (optional)

  • Returns: promise (resolves to contract address)

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()

  • Params: Address

  • Returns: promise (resolves to address)

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

setOwner()

  • Params: Address of new owner, DSProxy address (optional)

  • Returns: TransactionObject

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.

const service = maker.service('proxy');
// Forwarding proxy

function lockAndDraw(address tub_, bytes32 cup, uint wad) public payable {
  lock(tub_, cup);
  draw(tub_, cup, wad);
}
proxy registry
recover any funds
// 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
    }
  );
}
async function getProxy() {
  return maker.service('proxy').currentProxy();
}
async function buildProxy() {
  const proxyService = maker.service('proxy');
  if (!proxyService.currentProxy()) {
    return proxyService.build();
  }
}
const proxyAddress = await maker.service('proxy').ensureProxy();
const proxy = await maker.service('proxy').getProxyAddress('0x...');
const owner = await maker.service('proxy').getOwner('0x...');
await maker.service('proxy').setOwner(newOwner, proxyAddress);