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.
const service = Maker.create('test' {
smartContract: {
addressOverrides: {
PROXY_REGISTRY: '0xYourAddress'
}
}
});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
confirmedBlockCountThere 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.
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
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 blocksawait 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
}
}
})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.
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)
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.
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.
The event pipeline allows developers to easily create real-time applications by letting them listen for important state changes and lifecycle events.
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
<latest_block_when_emitted> - the current block at the time of the emitEvent 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);
})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:
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.
Plugins can add additional account types. There are currently two such plugins for hardware wallet support:
Install the to see this functionality in action.
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}
}
});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.
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.
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.
This convenience function will either return an existing proxy or create one.
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.
Params: Address
Returns: promise (resolves to address)
getOwner will query the proxy registry for the owner of a provided instance of DSProxy.
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);
}// 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);