xf00f / web3x

Ethereum TypeScript Client Library - for perfect types and tiny builds.
211 stars 27 forks source link

Not having `call` for transaction method is a mistake. #57

Open hickscorp opened 5 years ago

hickscorp commented 5 years ago

The fact that a contract.methods.aTransactionMethod(args...).send() is available but not a contract.methods.aTransactionMethod(args...).call() is a big problem for us, as it is very common practice to run a call "blank" before performing a real send transaction.

The reason behind this sort of choice is that revert messages cannot be read when they occur on a send - they can only be read when using call using direct bytecode ABI encoding.

An example of what this led us to do:

const REVERT_FUNCTION_ID = "0x08c379a0";

...

  safeSendTx = async (state: State, opts: any) => {
    if (!isInitialized(state)) {
      throw new Error("The provider isn't ready to relay transactions.");
    }
    // First, simulate the transaction for free.
    await this.simulateTxSend(state, opts);
    // If we're here, it's unlikelly that a revert will occur, as the simulation
    // didn't throw.
    const tx = await state.eth.sendTransaction(opts);
    const hash = await tx.getTxHash();
    try {
      return await tx.getReceipt();
    } catch (ex) {
      // Get some info about the failed transaction hash.
      const fullTx = await state.eth.getTransaction(hash);
      // As the transaction failed, we will try and simulate it again in the block
      // it failed to try and retrieve a "revert" message.
      await this.simulateTxSend(opts, fullTx.blockNumber || undefined);
      throw new Error("The transaction failed without a revert message.");
    }
  };

  simulateTxSend = async (state: State, opts: any, block?: BlockType) => {
    if (!isInitialized(state)) {
      throw new Error("The provider isn't ready to relay transactions.");
    }
    const txString = await state.eth.call(opts, block);
    if (!txString.startsWith(REVERT_FUNCTION_ID)) {
      return txString;
    }
    const enc = txString.substring(REVERT_FUNCTION_ID.length);
    const dec = abiCoder.decodeParameter("string", enc);
    throw new Error(dec);
  };

We have to call it like this:

          api
            .safeSendTx(state, {
              ...utils.chainGasOpts,
              data: transact.methods.approve(account, i).encodeABI(),
              from: state.account,
              to: utils.chainManifest.transact,
            })

This is really cumbersome - it would be great to have access to call even on transaction methods exactly like web3 and truffle do - allowing for shooting "blanks".

Please expose the option to call methods of contracts that are transactions.

xf00f commented 5 years ago

I can look into this for next patch release.

In the meantime, the call method should be present on the object, even if not exposed through the TypeScript interface. Underlying it's a Tx object: https://github.com/xf00f/web3x/blob/master/web3x/src/contract/tx.ts#L68

So with appropriate casting you should be able to use it without tonnes of hackery.

hickscorp commented 5 years ago

@xf00f Thank you for the feedback.

contract.methods.doStuff(...) gives me a TxSend<T>. How would I cast that into a TxCall<T>? The only way I found so far is:

// Inside a generic defining T from the original (method : TxSend<T>):
const callMethod : TxCall<T> = method as any;

Is this what you meant? I'm not sure - as if the underlying implementation changes, this would still compile and fail only at runtime...

uhhhh2 commented 5 years ago

Would it be a good idea to change the references to TxSend in the web3x-codegen project to either of:

The references to TxSend: