trufflesuite / drizzle-react-components-legacy

A set of useful components for common dapp UI elements.
95 stars 70 forks source link

Render Dynamic Contracts #87

Closed mwaeckerlin closed 5 years ago

mwaeckerlin commented 5 years ago

Render contracs that are dynamically instanciated using an address and an ABI, i.e. render the target contract in an array of addresses to contracts. Perhaps this will need a new class, sich as DynamicContract.js.

mwaeckerlin commented 5 years ago

Here is my suggestion, as I implemented it in my project. If you want, @honestbonsai, you may integrate my solution in drizzle-react-components (or I can do it for you, including tests and docu):

DynamicContracts.js

import React from "react";
import PropTypes from "prop-types";
import { drizzleConnect } from "drizzle-react";
import { ContractData } from "drizzle-react-components";

class DynamicContract extends React.Component {
  constructor(props, context) {
    super(props);

    var contractConfig = {
      contractName: this.props.contract || this.props.address,
      web3Contract: new context.drizzle.web3.eth.Contract(
        this.props.abi,
        this.props.address,
        this.props.options
      )
    };
    context.drizzle.addContract(contractConfig, this.props.events);
  }

render() {
    try {
      const contracts = this.props.store.getState().contracts;
      // Contract is not yet intialized.
      if (
        !contracts[this.props.contract || this.props.address] ||
        !contracts[this.props.contract || this.props.address].initialized
      ) {
        return <span>Initializing...</span>;
      }
      if (this.props.render)
        if (this.props.method)
          return (
            <ContractData
              contracts={contracts}
              contract={this.props.contract || this.props.address}
              method={this.props.method}
              methodArgs={this.props.methodArgs}
              sendArgs={this.props.sendArgs}
              render={this.props.render}
            />
          );
        else
          return this.props.render(this.props.contracts, this.props.contract || this.props.address);
      if (this.props.children) {
        const extraProps = {
          contracts: contracts,
          contract: this.props.contract || this.props.address
        };
        const mapper = function(child) {
          if (!React.isValidElement(child)) return child;
          return React.cloneElement(child, {
            ...extraProps,
            children: React.Children.map(child.props.children, mapper)
          });
        };
        return React.Children.map(this.props.children, mapper);
      }
      if (this.props.method)
        return (
          <ContractData
            contracts={contracts}
            contract={this.props.contract || this.props.address}
            method={this.props.method}
            methodArgs={this.props.methodArgs}
            sendArgs={this.props.sendArgs}
          />
        );
      return <>Contract: {this.props.contract || this.props.address}</>;
    } catch (e) {
      console.log("ERROR in DynamicContract render", e);
      return <div class="error">ERROR: {e.message}</div>;
    }
  }
}

DynamicContract.contextTypes = {
  drizzle: PropTypes.object
};

DynamicContract.propTypes = {
  contracts: PropTypes.object.isRequired,
  address: PropTypes.string.isRequired,
  abi: PropTypes.array.isRequired,
  contract: PropTypes.string,
  options: PropTypes.object,
  events: PropTypes.object,
  render: PropTypes.func
};

/*
 * Export connected component.
 */

const mapStateToProps = state => {
  return {
    contracts: state.contracts
  };
};

export default drizzleConnect(DynamicContract, mapStateToProps);

For the usage you have several options:

Example:

<DynamicContracs address="0x…" abi={abi}>
  X=<ContracData method="getX" />
</DynamicContracs>
honestbonsai commented 5 years ago

@mwaeckerlin Does this update when the props change? E.g. I pass in new props for address etc. I would expect the component to make a new dynamic contract. But I don't see anything to handle that. Correct me if I'm misunderstanding.

Right now it looks like it only works for 1 contract on initialization. New props should always update the component.

mwaeckerlin commented 5 years ago

@honestbonsai, watching props could be added the same way, as you do it e.g. in ContractData.

Yes, by design, you instanciate one dynamic contract per address.

I use it in the following context: A method on a contract, accessed using ContractData, returns a list of addresses of other contracts. In the render props, I iterate through this list do show information of the referenced contracts:

<ContractData
  contract="Parent"
  method="getChildren"
  render={children => (
    children.map(child => (
      <ul>
        <DynamicContract
          address={child}
          abi={childAbi}>
          <li key={child}>
            <ContractData method="getSomeText" /> // contract is set by DynamicContract
          </li>
        </DynamicContract>
      </ul>
    ))
  )}
/>
stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

mwaeckerlin commented 5 years ago

Are you interested to get it?

It works fine in our project …

honestbonsai commented 5 years ago

@mwaeckerlin Sorry for the delay.

There's a few things I think need to be addressed:

  1. context.drizzle.addContract should happen every time props change. Of course there would have to be a way to check we aren't adding contracts which have already been added once.
  2. render props vs children: The rest of the components in DRC use render props for custom rendering, so I think this one should align with that as well.

    In my opinion, the code for render props is more straight forward than the children version since we don't have to use lower level React apis, can just use simple components.

Thoughts?

stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] commented 5 years ago

This issue has been closed, but can be re-opened if further comments indicate that the problem persists. Feel free to tag maintainers if there is no reply to further comments.