package commonclient import ( "context" "fmt" "strings" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" ) const alreadyExistsError = "already exists" type WaiterOptions struct { // IgnoreAlreadyExistsError controls behavior for "already exists" error: // - If set to true, it indicates that "already exists" error is not a problem, we should // wait for transaction as usual (this is the behavior of neo-go [waiter.PollingBased]). // - If set to false, it indicates that "already exists" should be reported as an error. IgnoreAlreadyExistsError bool // VerifyExecResults controls whether waiter should ensure that transaction successfully // enters blockchain block. VerifyExecResults bool } // Waiter is a decorator on top of the standard [waiter.Waiter]. // It provides additional behavior (controlled by [WaiterOptions]) on top of the standard // functionality of awaiting transactions. type Waiter struct { waiter waiter.Waiter options WaiterOptions } var _ waiter.Waiter = (*Waiter)(nil) // NewWaiter decorates the specified waiter in a new [Waiter] instance. func NewWaiter(waiter waiter.Waiter, options WaiterOptions) *Waiter { return &Waiter{ waiter: waiter, options: options, } } // Wait allows to wait until transaction will be accepted to the chain. func (w *Waiter) Wait(h util.Uint256, vub uint32, err error) (*state.AppExecResult, error) { if !w.options.IgnoreAlreadyExistsError && errIsAlreadyExists(err) { return nil, err } result, err := w.waiter.Wait(h, vub, err) return w.examineExecResult(result, err) } // WaitAny waits until at least one of the specified transactions will be accepted // to the chain. func (w *Waiter) WaitAny(ctx context.Context, vub uint32, hashes ...util.Uint256) (*state.AppExecResult, error) { result, err := w.waiter.WaitAny(ctx, vub, hashes...) return w.examineExecResult(result, err) } func (w *Waiter) examineExecResult(result *state.AppExecResult, err error) (*state.AppExecResult, error) { if !w.options.VerifyExecResults { return result, err } // Waiting failed if err != nil { return result, err } if result.Execution.VMState != vmstate.Fault { // Transaction didn't fail, so we just return result "as is" return result, nil } // Transaction failed, we extract VM exception from it and report as an error if result.FaultException != "" { return result, fmt.Errorf("%s", result.FaultException) } return result, fmt.Errorf("transaction failed, stack=%v", result.Stack) } func errIsAlreadyExists(err error) bool { if err == nil { return false } return strings.Contains(strings.ToLower(err.Error()), alreadyExistsError) }