solana-labs / solana-web3.js

Solana JavaScript SDK
https://solana-labs.github.io/solana-web3.js
MIT License
2.2k stars 875 forks source link

A better way to resolve custom error messages #2019

Closed mikemaccana closed 3 weeks ago

mikemaccana commented 10 months ago

Motivation

A user is using web3.js, making transactions with instructions for the Token program. They recieve:

failed to send transaction: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x10

Which actually means, per the Token program's errors:

This token mint cannot freeze accounts

Example use case

This is a frequent show stopper for developers we've seen at Hacker Houses, where someone assumes there's no way to find an actual error. As a short term solution, DevRel added https://github.com/solana-developers/helpers?tab=readme-ov-file#getcustomerrormessage to our helpers library, but the same or better (and hopefully better is possible) solution should be available out of the box.

Details

Worst case: just have something like https://github.com/solana-developers/helpers?tab=readme-ov-file#getcustomerrormessage, code is at https://github.com/solana-developers/helpers/blob/main/src/index.ts#L14

Ideally: web3.js can dynamically fetch the errors for the specific program, and actually resolve the hex code to the real error message from the program as needed.

lorisleiva commented 10 months ago

Hey Mike, we're working on it for the new Web3.js. ☺️

Have you seen this? [Copy/pasting the relevant section below].

These create program functions will be generated for each program and will allow us to transform an hex code into an actual program error.


resolveTransactionError()

This function takes a raw error caused by a transaction failure and attempts to resolve it into a custom program error.

For this to work, the resolveTransactionError function also needs the following parameters:

Note that, if the error cannot be resolved into a custom program error, the original error is returned as-is.

// Store your programs.
const programs = [createSplSystemProgram(), createSplComputeBudgetProgram(), createSplAddressLookupTableProgram()];

try {
    // Send and confirm your transaction.
} catch (error) {
    throw resolveTransactionError(error, transaction, programs);
}
mikemaccana commented 10 months ago

Great minds etc! 😃 Thanks @lorisleiva !

Would it be possible to allow resolveTransactionError() to look up the program by ID and get the errors, removing the need for programs?

lorisleiva commented 10 months ago

Would it be possible to allow resolveTransactionError() to look up the program by ID and get the errors, removing the need for programs?

The resolveTransactionError function already identifies the throwing program by ID and its custom error code. However we need a source of information where, given a program ID and an error code, we are provided with the custom program error that makes sense to the end user — namely, we need the name and message behind that error code.

That source of information here is being explicitly requested by the function as otherwise we would need a centralised registry of program error codes to fetch from. Is that what you are referring to?

P.S.: You might be interested in point [E] of the thread.

mikemaccana commented 10 months ago

we would need a centralised registry of program error codes to fetch from. Is that what you are referring to?

Exactly! Ie if the IDL is published, we fetch the errors from the IDL. MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD error 0x0 is "Wrong reserve owner. Must be a system account" because https://explorer.solana.com/address/MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD/anchor-program says so.

If the IDL is not published, we tell this explicitly to the user and return Custom Program Error and the hex number same as present.

Downsides: we have an HTTP request to resolve the error. Users could turn off custom errors handling if they wanted to to disable this behavior though.

Upsides: we have useful errors by default and don't ask users to provide/maintain a list of all the programs they want to use, or have something that recursively gets the programs another program may CPI to per [E]

lorisleiva commented 10 months ago

Gotcha! I think having an additional asynchronous helper method that uses the Anchor IDL registry for that purpose makes total sense.

However, I do think we should keep the synchronous method for situations where we just want to use the information provided by the generated clients to avoid an extra HTTP call that may not even resolve.

I'd also like to explore a plugin ecosystem on top of the web3.js library that would help bind all the components together. For instance, this would be much easier to handle with a program repository plugin (which is how Umi handles this problem).

dtmrc commented 9 months ago

would it be possible to bubble custom Error classes in the case where sendAndConfirmRawTransaction() returns strings such as Raw transaction ${signature} failed ({"err":{"InstructionError":[2,{"Custom":30}]}}), or failed to send transaction: Transaction simulation failed: Blockhash not found.

it is less than ideal to have to parse message strings everywhere when implementing error handling. some of them are rpc/sending related, some might be network, some may or may not be program specific. for eg. the custom error class SolanaJSONRPCError does not get exposed or bubble up to the client, so even though logic exists for error type handling, i have to then do a bunch of string fragment matching to re-type the error on each send invocation.

steveluscher commented 7 months ago

We now have first-class SolanaErrors being thrown for custom program errors.

try {
    // Something.
} catch (e) {
    if (isSolanaError(e, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE)) {
        // Now TypeScript knows that you have e.context.logs and e.context.returnData and stuff.
        // But also…
        if (isSolanaError(e.cause, SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM)) {
            // Now typescript has e.cause.context.code and e.cause.context.index
        }
    }
}
mlshv commented 7 months ago

@steveluscher When is it going to be released? I see it's in the technical preview version, but not the stable one. I'd love to use the TP in my project, but I've only found one demo and there's no other examples or documentation. Am I missing something?

buffalojoec commented 7 months ago

@mlshv Have you checked out this README in the main library? It's a little hidden, but we've kept that up to date. https://github.com/solana-labs/solana-web3.js/blob/master/packages/library/README.md

Besides that, some of the packages' READMEs are detailed, and some aren't. Admittedly we don't have end-to-end docs like we'd want to have, but that's mainly because everything has been changing so much. Perhaps Stack Exchange can be a decent medium until then?

Also, we have published a "Technology Preview 2", which contains the custom errors, as well as the errors package itself. https://www.npmjs.com/package/@solana/errors

steveluscher commented 3 weeks ago

Errors are so much better now in the 2.0 line of web3.js, and the addition of isProgramError() here rounds it out.

Specifically, addressing the use case in your original post @mikemaccana, codegenerated clients now have their own specialized version of isProgramError(), like this.

If there's anything more to do here, specifically, feel free to describe it in a new issue!

dtmrc commented 2 weeks ago

@steveluscher you are a true gent. i am curious tho, how would you approach network related error handling vs program error handling? most of the variable conditions at the rpc & network layer tend to cause the most issues within the code and are relevant for the UI to handle gracefully

steveluscher commented 2 weeks ago

Using isSolanaError() you can disambiguate between network errors and other kinds of errors.

try {
    const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
} catch(e) {
    if (isSolanaError(e, SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR)) {
        console.error('o no bad network', e.cause);
        // Do something specific about an HTTP error, like retry.
    } else {
        throw e;
    }
}
github-actions[bot] commented 1 week ago

Because there has been no activity on this issue for 7 days since it was closed, it has been automatically locked. Please open a new issue if it requires a follow up.