@windingtree/wt-write-api
Version:
API to write data to the Winding Tree platform
378 lines (347 loc) • 9.98 kB
JavaScript
const _ = require('lodash');
const Web3Utils = require('web3-utils');
const {
validateDescription, validateRatePlans, validateAvailability, validateNotifications, validateBooking,
validateDataUri, validateGuarantee, validateAddress, validateContacts, validateOrgName,
} = require('./validators');
/* A declarative description of hotel data. */
const DATA_INDEX_FIELDS = [
{
name: 'description',
dataIndexName: 'descriptionUri',
required: true,
pointer: true, // Is this a pointer to another subdocument?
validator: validateDescription,
},
{
name: 'ratePlans',
dataIndexName: 'ratePlansUri',
required: false,
pointer: true,
validator: validateRatePlans,
},
{
name: 'availability',
dataIndexName: 'availabilityUri',
required: false,
pointer: true,
validator: validateAvailability,
},
{
name: 'notifications',
dataIndexName: 'notificationsUri',
required: false,
pointer: false,
validator: validateNotifications,
},
{
name: 'booking',
dataIndexName: 'bookingUri',
required: false,
pointer: false,
validator: validateBooking,
},
{
name: 'defaultLocale',
dataIndexName: 'defaultLocale',
required: false,
pointer: false,
validator: () => {}, // TODO
},
{
name: 'guarantee',
dataIndexName: 'guarantee',
required: false,
pointer: false,
validator: validateGuarantee,
},
];
const NO_DATA_INDEX_FIELDS = [
{
name: 'orgJson',
required: true,
pointer: false,
validator: validateDataUri,
},
{
name: 'hash',
required: true,
pointer: false,
validator: () => {}, // TODO
},
];
const LEGAL_ENTITY_FIELDS = [
{
name: 'name',
dataIndexName: 'name',
required: true,
pointer: false,
validator: validateOrgName,
},
{
name: 'address',
dataIndexName: 'address',
required: true,
pointer: false,
validator: validateAddress,
},
{
name: 'contact',
dataIndexName: 'contact',
required: true,
pointer: false,
validator: validateContacts,
},
];
const DATA_INDEX_FIELD_NAMES = _.map(DATA_INDEX_FIELDS, 'name');
const NO_DATA_INDEX_FIELD_NAMES = _.map(NO_DATA_INDEX_FIELDS, 'name');
const LEGAL_ENTITY_FIELD_NAMES = _.map(LEGAL_ENTITY_FIELDS, 'name');
/**
* Gateway to the WT platform. Wraps wtLibs.
*/
class WT {
constructor (wtLibs, entrypointAddress) {
this.wtLibs = wtLibs;
this.entrypointAddress = entrypointAddress;
}
_getEntrypoint () {
return this.wtLibs.getEntrypoint(this.entrypointAddress);
}
async _getFactory () {
return this._getEntrypoint().getOrganizationFactory();
}
async _getDirectory () {
return this._getEntrypoint().getSegmentDirectory('hotels');
}
_getOrganization (address) {
return this.wtLibs.getUpdateableOrganization(address);
}
async getDirectoryAddress () {
if (!this._directory) {
this._directory = await this._getDirectory();
}
return this._directory.address;
}
async getFactoryAddress () {
if (!this._factory) {
this._factory = await this._getFactory();
}
return this._factory.address;
}
async _addHotel (withWallet, orgJsonUri, orgJsonHash) {
const factory = await this._getFactory();
return withWallet(async (wallet) => {
const { organization, transactionData, eventCallbacks } = await factory.createAndAddOrganization({
owner: wallet.getAddress(),
orgJsonUri: orgJsonUri,
orgJsonHash: orgJsonHash,
}, await this.getDirectoryAddress());
await wallet.signAndSendTransaction(transactionData, eventCallbacks);
// Once the transaction is mined, one of the callbacks
// sets the address.
return (await organization).address;
});
}
async _updateHotel (withWallet, orgJsonUri, orgJsonHash, hotelAddress) {
const organization = this._getOrganization(hotelAddress);
organization.orgJsonUri = orgJsonUri;
organization.orgJsonHash = orgJsonHash;
await withWallet(async (wallet) => {
const transactionDataList = await organization.updateOnChainData({
from: wallet.getAddress(),
});
const transactions = [];
for (const { transactionData, eventCallbacks } of transactionDataList) {
transactions.push(wallet.signAndSendTransaction(transactionData, eventCallbacks));
}
await Promise.all(transactions);
});
return hotelAddress;
}
/**
* Return a wallet.
*
* @param {Object} walletData
*/
createWallet (walletData) {
return this.wtLibs.createWallet(walletData);
}
/**
* Create or update hotel with the given orgJsonUri.
*
* @param {Function} withWallet
* @param {string} orgJsonUri
* @param {string} orgJsonHash
* @param {string} hotelAddress (optional)
* @return {Promise<string>} Etherum address of the uploaded
* data.
*/
upload (withWallet, orgJsonUri, orgJsonHash, hotelAddress) {
if (hotelAddress) {
return this._updateHotel(withWallet, orgJsonUri, orgJsonHash, hotelAddress);
}
return this._addHotel(withWallet, orgJsonUri, orgJsonHash);
}
getContentsHash (contents) {
return Web3Utils.soliditySha3(contents);
}
/**
* Remove the hotel from WT index.
*
* @param {Function} withWallet
* @param {string} hotelAddress
* @return {Promise<void>}
*/
async remove (withWallet, hotelAddress) {
const directory = this._getDirectory();
await withWallet(async (wallet) => {
const { transactionData, eventCallbacks } = await directory.remove({
address: hotelAddress,
owner: wallet.getAddress(),
});
await wallet.signAndSendTransaction(transactionData, eventCallbacks);
});
}
async _getRawOrgJson (hotelAddress) {
const hotel = await this._getOrganization(hotelAddress);
return hotel.orgJson;
}
async _formatOffChainDataIndex (rawIndex) {
const contents = {},
indexContents = await rawIndex.contents;
for (const field of DATA_INDEX_FIELDS) {
const name = field.dataIndexName;
if (field.pointer) {
contents[name] = _.get(indexContents, [name, 'ref']);
} else {
contents[name] = _.get(indexContents, [name]);
}
}
contents.dataFormatVersion = _.get(indexContents, 'dataFormatVersion');
return {
ref: rawIndex.ref,
contents: contents,
};
}
async _formatOffChainOrgJson (rawOrgJson) {
return {
ref: rawOrgJson.ref,
contents: await rawOrgJson.contents,
};
}
async getOrgJson (hotelAddress) {
return this._formatOffChainOrgJson(await this._getRawOrgJson(hotelAddress));
}
async getOrgJsonHash (hotelAddress) {
const hotel = await this._getOrganization(hotelAddress);
return hotel.orgJsonHash;
}
/**
* Return the hotel's data index.
*
* @param {string} hotelAddress
* @return {Promise<Object>}
*/
async getDataIndex (hotelAddress) {
const addr = await this._getRawDataIndex(hotelAddress);
return addr ? this._formatOffChainDataIndex(addr) : {};
}
async _getRawDataIndex (hotelAddress) {
const hotel = this._getOrganization(hotelAddress);
const apis = await hotel.getWindingTreeApi();
return apis.hotel[0] || {};
}
async resolveUnregisteredDataIndex (orgJsonUri) {
const hotel = this._getOrganization();
hotel.orgJsonUri = orgJsonUri;
const apis = await hotel.getWindingTreeApi();
return apis.hotel[0] ? this._formatOffChainDataIndex(apis.hotel[0]) : {};
}
/**
* Return resolved hotel data.
*
* @param {string} hotelAddress
* @param {Array} fieldNames (required) Only include these fields.
* @return {Promise<Object>}
*/
async getDocuments (hotelAddress, fieldNames) {
const orgJson = await this._getRawOrgJson(hotelAddress);
const orgJsonContents = await orgJson.contents,
rawIndex = await this._getRawDataIndex(hotelAddress),
data = {},
contents = await rawIndex.contents;
for (const fieldName of fieldNames) {
const dataIndexName = (DATA_INDEX_FIELDS.find((f) => f.name === fieldName)).dataIndexName;
const doc = contents[dataIndexName];
if (doc) {
if (doc.toPlainObject) {
data[`${fieldName}`] = (await doc.toPlainObject()).contents;
} else { // No pointer.
data[`${fieldName}`] = doc;
}
}
}
// TODO this is hacky and should be refactored in a similar way to hotel data
return {
legalEntity: orgJsonContents.legalEntity,
guarantee: orgJsonContents.guarantee,
hotel: data,
};
}
/**
* Return true, if the provided ethereum address is non-zero
* and valid, false otherwise.
*
* @param {string} address
*/
isValidAddress (address) {
if (!address || !Web3Utils.isAddress(address)) {
return false;
}
return String(address) !== '0x0000000000000000000000000000000000000000';
}
/**
* Transfer hotel to a different manager.
*
* @param {Function} withWallet
* @param {string} hotelAddress
* @param {string} managerAddress
* @return {Promise<void>}
*/
async transferHotel (withWallet, hotelAddress, managerAddress) {
const hotel = this._getOrganization(hotelAddress);
await withWallet(async (wallet) => {
const { transactionData, eventCallbacks } = await hotel.transferOnChainOwnership(managerAddress, {
from: wallet.getAddress(),
});
await wallet.signAndSendTransaction(transactionData, eventCallbacks);
});
}
};
let _WT;
/**
* Get the previously set WT instance.
*/
function get () {
if (!_WT) {
throw new Error('No WT instance has been set!');
}
return _WT;
}
/**
* Set WT instance during runtime.
*/
function set (wt) {
_WT = wt;
}
module.exports = {
WT,
get,
set,
DATA_INDEX_FIELDS,
DATA_INDEX_FIELD_NAMES,
NO_DATA_INDEX_FIELDS,
NO_DATA_INDEX_FIELD_NAMES,
LEGAL_ENTITY_FIELDS,
LEGAL_ENTITY_FIELD_NAMES,
};