UNPKG

@windingtree/wt-js-libs

Version:

Javascript libraries to interact with the Winding Tree contracts

241 lines (223 loc) 7.82 kB
import RemotelyBackedDataset from './remotely-backed-dataset'; import Organization from './organization'; import StoragePointer from './storage-pointer'; import { InputDataError, SmartContractInstantiationError } from './errors'; /** * Wrapper class for an organization backed by a smart contract on * Ethereum that's holding `orgJsonUri` pointer to its data. * * This is meant as a read/write wrapper. If you want to change some field, * just use its setter and call `updateOnChainData` when ready. This will produce * as many transactions as necessary for you to execute on chain. * */ export class UpdateableOnChainOrganization extends Organization { static createInstance (web3Utils, web3Contracts, address) { const org = new UpdateableOnChainOrganization(web3Utils, web3Contracts, address); org.initialize(); return org; } initialize () { this.onChainDataset = RemotelyBackedDataset.createInstance(); this.onChainDataset.bindProperties({ fields: { _orgJsonUri: { remoteGetter: async () => { return (await this._getContractInstance()).methods.getOrgJsonUri().call(); }, remoteSetter: this._editOrgJsonDataOnChain('orgJsonUri', 'changeOrgJsonUri').bind(this), }, _orgJsonHash: { remoteGetter: async () => { return (await this._getContractInstance()).methods.getOrgJsonHash().call(); }, remoteSetter: this._editOrgJsonDataOnChain('orgJsonHash', 'changeOrgJsonHash').bind(this), }, _owner: { remoteGetter: async () => { return (await this._getContractInstance()).methods.owner().call(); }, }, _associatedKeys: { remoteGetter: async () => { return (await this._getContractInstance()).methods.getAssociatedKeys().call(); }, }, }, }, this); this._initialized = true; if (this.address) { this.onChainDataset.markDeployed(); } } async _getContractInstance () { if (!this.address) { throw new SmartContractInstantiationError('Cannot get Organization instance without address'); } if (!this.contractInstance) { this.contractInstance = await this.web3Contracts.getUpdateableOrganizationInstance(this.address); } return this.contractInstance; } get orgJson () { return (async () => { if (!this._orgJson) { // we leverage StoragePointer to make this work with various off-chain storages // no direct linked subdocuments though for now this._orgJson = StoragePointer.createInstance(await this.orgJsonUri, {}); } return this._orgJson; })(); } get orgJsonUri () { if (!this._initialized) { return; } return (async () => { const orgJsonUri = await this._orgJsonUri; return orgJsonUri; })(); } get orgJsonHash () { if (!this._initialized) { return; } return (async () => { const orgJsonHash = await this._orgJsonHash; return orgJsonHash; })(); } get owner () { if (!this._initialized) { return; } return (async () => { const manager = await this._owner; return manager; })(); } get associatedKeys () { if (!this._initialized) { return; } return (async () => { const associatedKeys = await this._associatedKeys; return associatedKeys; })(); } set orgJsonUri (newOrgJsonUri) { if (!newOrgJsonUri) { throw new InputDataError( 'Cannot update Organization: Cannot set orgJsonUri when it is not provided' ); } if (typeof newOrgJsonUri === 'string' && !newOrgJsonUri.match(/([a-z-]+):\/\//)) { throw new InputDataError( 'Cannot update Organization: Cannot set orgJsonUri with invalid format' ); } if (newOrgJsonUri !== this._orgJsonUri) { this._orgJson = null; } this._orgJsonUri = newOrgJsonUri; } set orgJsonHash (newOrgJsonHash) { if (!newOrgJsonHash) { throw new InputDataError( 'Cannot update Organization: Cannot set orgJsonHash when it is not provided' ); } if (typeof newOrgJsonHash === 'string' && !newOrgJsonHash.match(/^0x/)) { throw new InputDataError( 'Cannot update Organization: Cannot set orgJsonHash with invalid format' ); } this._orgJsonHash = newOrgJsonHash; } async setLocalData (newData) { const newOrgJsonUri = await newData.orgJsonUri; if (newOrgJsonUri) { this.orgJsonUri = newOrgJsonUri; } const newOrgJsonHash = await newData.orgJsonHash; if (newOrgJsonHash) { this.orgJsonHash = newOrgJsonHash; } } /** * Factory for update on chain data, handles orgJsonUri and orgJsonHash. * It can decide by checking onChainDataset internal's state if to use * a single-field update contract method or be efficient and use only one * to update both fields in a single transaction. * * @param {string} field name to be updated * @param {string} base contract method name that updates that single field * @return {function} function that can prepare tx data */ _editOrgJsonDataOnChain (field, baseMethod) { const update = async (contract, fieldName, baseMethodName) => { if ( this.onChainDataset.getFieldState('_orgJsonUri') === 'dirty' && this.onChainDataset.getFieldState('_orgJsonHash') === 'dirty' ) { return contract.methods.changeOrgJsonUriAndHash(await this.orgJsonUri, await this.orgJsonHash); } return contract.methods[baseMethodName](await this[fieldName]); }; return async function (transactionOptions) { const contract = await this._getContractInstance(); const updateMethod = await update(contract, field, baseMethod); const estimate = updateMethod.estimateGas({ from: transactionOptions.from, }); const txData = updateMethod.encodeABI({ from: transactionOptions.from, }); const transactionData = { nonce: await this.web3Utils.determineCurrentAddressNonce(transactionOptions.from), data: txData, from: transactionOptions.from, to: this.address, gas: this.web3Utils.applyGasModifier(await estimate), }; return { organization: this, transactionData: transactionData, }; }; } async updateOnChainData (transactionOptions) { // TODO somehow check that the contract supports this interface // pre-check if contract is available at all and fail fast await this._getContractInstance(); // We have to clone options for each dataset as they may get modified // along the way return this.onChainDataset.updateRemoteData(Object.assign({}, transactionOptions)); } async transferOnChainOwnership (newOwner, transactionOptions) { if (!this.onChainDataset.isDeployed()) { throw new SmartContractInstantiationError('Cannot transfer Organization: not deployed'); } const contract = await this._getContractInstance(); const estimate = contract.methods.transferOwnership(newOwner).estimateGas(); const txData = contract.methods.transferOwnership(newOwner).encodeABI(); const transactionData = { nonce: await this.web3Utils.determineCurrentAddressNonce(transactionOptions.from), data: txData, from: transactionOptions.from, to: this.address, gas: this.web3Utils.applyGasModifier(await estimate), }; const eventCallbacks = { onReceipt: (receipt) => { this._owner = newOwner; }, }; return { organization: this, transactionData: transactionData, eventCallbacks: eventCallbacks, }; } } export default UpdateableOnChainOrganization;