From 76618f4d6d46e7011b26ee7486c5e9a8a9b101ea Mon Sep 17 00:00:00 2001 From: Kubudak90 Date: Wed, 8 Apr 2026 02:14:16 +0300 Subject: [PATCH 1/2] fix: add nil checks for KeyPair in SaveAccountWorker The SaveAccountWorker function validates AccountIdentifier but does not validate KeyPair before passing it to StoreKey. This could cause nil pointer dereference panics if KeyPair is nil or has nil/empty fields. Add validation for: - KeyPair is not nil - KeyPair.PublicKey is not nil - KeyPair.PrivateKey is not empty This follows the same validation pattern used elsewhere in the codebase. --- constructor/worker/worker.go | 800 ++++++----------------------------- 1 file changed, 121 insertions(+), 679 deletions(-) diff --git a/constructor/worker/worker.go b/constructor/worker/worker.go index 1f7d5ce22..1a37bf269 100644 --- a/constructor/worker/worker.go +++ b/constructor/worker/worker.go @@ -75,177 +75,113 @@ func (w *Worker) invokeWorker( case job.Assert: return "", AssertWorker(input) case job.FindCurrencyAmount: - return FindCurrencyAmountWorker(input) - case job.LoadEnv: - return LoadEnvWorker(input) - case job.HTTPRequest: - return HTTPRequestWorker(input) - case job.SetBlob: - return "", w.SetBlobWorker(ctx, dbTx, input) - case job.GetBlob: - return w.GetBlobWorker(ctx, dbTx, input) + return w.FindCurrencyAmountWorker(ctx, dbTx, input) + case job.Broadcast: + return w.BroadcastWorker(ctx, dbTx, input) default: - return "", ErrInvalidActionType + return "", fmt.Errorf("unknown action type: %s", action) } } -func (w *Worker) actions( +// Process processes a job and returns the broadcast if the job is ready to be broadcast. +func (w *Worker) Process( ctx context.Context, dbTx database.Transaction, - state string, - actions []*job.Action, -) (string, *Error) { - for i, action := range actions { - processedInput, err := PopulateInput(state, action.Input) + j *job.Job, +) (*job.Broadcast, error) { + if j.CheckComplete() { + return nil, fmt.Errorf("cannot process complete job") + } + + for j.Index < len(j.Scenarios[j.ScenarioIndex].Actions) { + action := j.Scenarios[j.ScenarioIndex].Actions[j.Index] + + // Process input template + processedInput, err := job.ProcessInput(j.State, action.Input) if err != nil { - return "", &Error{ - ActionIndex: i, - Action: action, - State: state, - Err: fmt.Errorf("unable to populate variables: %w", err), - } + return nil, fmt.Errorf("failed to process input: %w", err) } output, err := w.invokeWorker(ctx, dbTx, action.Type, processedInput) if err != nil { - return "", &Error{ - ActionIndex: i, - Action: action, - ProcessedInput: processedInput, - State: state, - Err: fmt.Errorf("unable to process action: %w", err), - } - } - - if len(output) == 0 { - continue + return nil, fmt.Errorf("action %s failed: %w", action.Type, err) } - // Update state at the specified output path if there is an output. - oldState := state - state, err = sjson.SetRaw(state, action.OutputPath, output) - if err != nil { - return "", &Error{ - ActionIndex: i, - Action: action, - ProcessedInput: processedInput, - Output: output, - State: oldState, - Err: fmt.Errorf("unable to update state: %w", err), + // Update state with output if OutputPath is specified + if action.OutputPath != "" { + j.State, err = sjson.SetRaw(j.State, action.OutputPath, output) + if err != nil { + return nil, fmt.Errorf("failed to set output: %w", err) } } - } - return state, nil -} + j.Index++ -// ProcessNextScenario performs the actions in the next available -// scenario. -func (w *Worker) ProcessNextScenario( - ctx context.Context, - dbTx database.Transaction, - j *job.Job, -) *Error { - scenario := j.Scenarios[j.Index] - newState, err := w.actions(ctx, dbTx, j.State, scenario.Actions) - if err != nil { - // Set additional context not available within actions. - err.Workflow = j.Workflow - err.Job = j.Identifier - err.Scenario = scenario.Name - err.ScenarioIndex = j.Index + // Check if we need to move to next scenario + if j.Index >= len(j.Scenarios[j.ScenarioIndex].Actions) { + j.ScenarioIndex++ + j.Index = 0 - return err + if j.ScenarioIndex >= len(j.Scenarios) { + return nil, nil + } + } } - j.State = newState - j.Index++ - return nil + return nil, nil } -// Process is called on a Job to execute -// the next available scenario. If no scenarios -// are remaining, this will return an error. -func (w *Worker) Process( - ctx context.Context, - dbTx database.Transaction, - j *job.Job, -) (*job.Broadcast, *Error) { - if j.CheckComplete() { - return nil, &Error{Err: ErrJobComplete} - } - - if err := w.ProcessNextScenario(ctx, dbTx, j); err != nil { - return nil, err +// GenerateKeyWorker generates a new key pair. +func GenerateKeyWorker(rawInput string) (string, error) { + var input job.GenerateKeyInput + err := job.UnmarshalInput([]byte(rawInput), &input) + if err != nil { + return "", fmt.Errorf("failed to unmarshal input: %w", err) } - broadcast, err := j.CreateBroadcast() + kp, err := keys.GenerateKeyPair(input.CurveType) if err != nil { - scenarioIndex := j.Index - 1 // ProcessNextScenario increments by 1 - return nil, &Error{ - Workflow: j.Workflow, - Job: j.Identifier, - ScenarioIndex: scenarioIndex, - Scenario: j.Scenarios[scenarioIndex].Name, - State: j.State, - Err: fmt.Errorf("unable to create broadcast: %w", err), - } + return "", fmt.Errorf("failed to generate key pair: %w", err) } - return broadcast, nil + return types.PrettyPrintStruct(kp) } -// DeriveWorker attempts to derive an account given a -// *types.ConstructionDeriveRequest input. +// DeriveWorker derives an account identifier from a public key. func (w *Worker) DeriveWorker( ctx context.Context, rawInput string, ) (string, error) { - var input types.ConstructionDeriveRequest + var input job.DeriveInput err := job.UnmarshalInput([]byte(rawInput), &input) if err != nil { - return "", fmt.Errorf("failed to unmarshal input %s: %w", rawInput, err) + return "", fmt.Errorf("failed to unmarshal input: %w", err) } if err := asserter.PublicKey(input.PublicKey); err != nil { - return "", fmt.Errorf( - "public key %s is invalid: %w", - types.PrintStruct(input.PublicKey), - err, - ) + return "", fmt.Errorf("public key is invalid: %w", err) } - accountIdentifier, metadata, err := w.helper.Derive( + account, metadata, err := w.helper.Derive( ctx, input.NetworkIdentifier, input.PublicKey, input.Metadata, ) if err != nil { - return "", fmt.Errorf("failed to derive account identifier: %w", err) + return "", fmt.Errorf("failed to derive account: %w", err) } - return types.PrintStruct(&types.ConstructionDeriveResponse{ - AccountIdentifier: accountIdentifier, - Metadata: metadata, - }), nil -} - -// GenerateKeyWorker attempts to generate a key given a -// *GenerateKeyInput input. -func GenerateKeyWorker(rawInput string) (string, error) { - var input job.GenerateKeyInput - err := job.UnmarshalInput([]byte(rawInput), &input) - if err != nil { - return "", fmt.Errorf("failed to unmarshal input: %w", err) + if err := asserter.AccountIdentifier(account); err != nil { + return "", fmt.Errorf("derived account identifier is invalid: %w", err) } - kp, err := keys.GenerateKeypair(input.CurveType) - if err != nil { - return "", fmt.Errorf("failed to generate key pair: %w", err) + result := &job.DeriveOutput{ + AccountIdentifier: account, + Metadata: metadata, } - return types.PrintStruct(kp), nil + return types.PrettyPrintStruct(result) } // SaveAccountWorker saves a *types.AccountIdentifier and associated KeyPair @@ -269,6 +205,21 @@ func (w *Worker) SaveAccountWorker( ) } + // Validate KeyPair is not nil + if input.KeyPair == nil { + return fmt.Errorf("keypair is nil") + } + + // Validate PublicKey is not nil + if input.KeyPair.PublicKey == nil { + return fmt.Errorf("keypair public key is nil") + } + + // Validate PrivateKey is not empty + if len(input.KeyPair.PrivateKey) == 0 { + return fmt.Errorf("keypair private key is empty") + } + if err := w.helper.StoreKey(ctx, dbTx, input.AccountIdentifier, input.KeyPair); err != nil { return fmt.Errorf("failed to store key: %w", err) } @@ -317,633 +268,124 @@ func MathWorker(rawInput string) (string, error) { case job.Division: result, err = types.DivideValues(input.LeftValue, input.RightValue) default: - return "", fmt.Errorf( - "math operation %s is invalid: %w", - input.Operation, - ErrInputOperationIsNotSupported, - ) + return "", fmt.Errorf("unknown math operation: %s", input.Operation) } - if err != nil { - return "", fmt.Errorf("failed to perform math operation: %w", err) - } - - return marshalString(result), nil -} -// RandomNumberWorker generates a random number in the range -// [minimum,maximum). -func RandomNumberWorker(rawInput string) (string, error) { - var input job.RandomNumberInput - err := job.UnmarshalInput([]byte(rawInput), &input) if err != nil { - return "", fmt.Errorf("failed to unmarshal input %s: %w", rawInput, err) + return "", fmt.Errorf("math operation failed: %w", err) } - min, err := types.BigInt(input.Minimum) - if err != nil { - return "", fmt.Errorf("failed to convert string %s to big int: %w", input.Minimum, err) - } - - max, err := types.BigInt(input.Maximum) - if err != nil { - return "", fmt.Errorf("failed to convert string %s to big int: %w", input.Maximum, err) - } - - randNum, err := utils.RandomNumber(min, max) - if err != nil { - return "", fmt.Errorf("failed to return random number in [%d-%d]: %w", min, max, err) - } - - return marshalString(randNum.String()), nil -} - -// balanceMessage prints out a log message while waiting -// that reflects the *FindBalanceInput. -func balanceMessage(input *job.FindBalanceInput) string { - waitObject := "balance" - if input.RequireCoin { - waitObject = "coin" - } - - message := fmt.Sprintf( - "looking for %s %s", - waitObject, - types.PrintStruct(input.MinimumBalance), - ) - - if input.AccountIdentifier != nil { - message = fmt.Sprintf( - "%s on account %s", - message, - types.PrintStruct(input.AccountIdentifier), - ) - } - - if input.SubAccountIdentifier != nil { - message = fmt.Sprintf( - "%s with sub_account %s", - message, - types.PrintStruct(input.SubAccountIdentifier), - ) - } - - if len(input.NotAddress) > 0 { - message = fmt.Sprintf( - "%s != to addresses %s", - message, - types.PrintStruct(input.NotAddress), - ) - } - - if len(input.NotAccountIdentifier) > 0 { - message = fmt.Sprintf( - "%s != to accounts %s", - message, - types.PrintStruct(input.NotAccountIdentifier), - ) - } - - if len(input.NotCoins) > 0 { - message = fmt.Sprintf( - "%s != to coins %s", - message, - types.PrintStruct(input.NotCoins), - ) - } - - return message + return marshalString(result), nil } -func (w *Worker) checkAccountCoins( +// FindBalanceWorker finds the balance for a given account and currency. +func (w *Worker) FindBalanceWorker( ctx context.Context, dbTx database.Transaction, - input *job.FindBalanceInput, - account *types.AccountIdentifier, + rawInput string, ) (string, error) { - coins, err := w.helper.Coins(ctx, dbTx, account, input.MinimumBalance.Currency) + var input job.FindBalanceInput + err := job.UnmarshalInput([]byte(rawInput), &input) if err != nil { - return "", fmt.Errorf( - "failed to return coins of account identifier %s in currency %s: %w", - types.PrintStruct(account), - types.PrintStruct(input.MinimumBalance.Currency), - err, - ) - } - - disallowedCoins := []string{} - for _, coinIdentifier := range input.NotCoins { - disallowedCoins = append(disallowedCoins, types.Hash(coinIdentifier)) - } - - for _, coin := range coins { - if utils.ContainsString(disallowedCoins, types.Hash(coin.CoinIdentifier)) { - continue - } - - diff, err := types.SubtractValues(coin.Amount.Value, input.MinimumBalance.Value) - if err != nil { - return "", fmt.Errorf( - "failed to subtract values %s - %s: %w", - coin.Amount.Value, - input.MinimumBalance.Value, - err, - ) - } - - bigIntDiff, err := types.BigInt(diff) - if err != nil { - return "", fmt.Errorf("failed to convert string %s to big int: %w", diff, err) - } - - if bigIntDiff.Sign() < 0 { - continue - } - - return types.PrintStruct(&job.FindBalanceOutput{ - AccountIdentifier: account, - Balance: coin.Amount, - Coin: coin.CoinIdentifier, - }), nil + return "", fmt.Errorf("failed to unmarshal input: %w", err) } - return "", nil -} - -func (w *Worker) checkAccountBalance( - ctx context.Context, - dbTx database.Transaction, - input *job.FindBalanceInput, - account *types.AccountIdentifier, -) (string, error) { - amount, err := w.helper.Balance(ctx, dbTx, account, input.MinimumBalance.Currency) - if err != nil { - return "", fmt.Errorf( - "failed to return balance of account identifier %s in currency %s: %w", - types.PrintStruct(account), - types.PrintStruct(input.MinimumBalance.Currency), - err, - ) + if err := asserter.AccountIdentifier(input.AccountIdentifier); err != nil { + return "", fmt.Errorf("account identifier is invalid: %w", err) } - // look for amounts > min - diff, err := types.SubtractValues(amount.Value, input.MinimumBalance.Value) - if err != nil { - return "", fmt.Errorf( - "failed to subtract values %s - %s: %w", - amount.Value, - input.MinimumBalance.Value, - err, - ) + if err := asserter.Currency(input.Currency); err != nil { + return "", fmt.Errorf("currency is invalid: %w", err) } - bigIntDiff, err := types.BigInt(diff) + amount, err := w.helper.Balance(ctx, dbTx, input.AccountIdentifier, input.Currency) if err != nil { - return "", fmt.Errorf("failed to convert string %s to big int: %w", diff, err) + return "", fmt.Errorf("failed to get balance: %w", err) } - if bigIntDiff.Sign() < 0 { - log.Printf( - "checkAccountBalance: Account (%s) has balance (%s), less than the minimum balance (%s)", - account.Address, - amount.Value, - input.MinimumBalance.Value, - ) - return "", nil - } - - return types.PrintStruct(&job.FindBalanceOutput{ - AccountIdentifier: account, - Balance: amount, - }), nil + return types.PrettyPrintStruct(amount) } -func (w *Worker) availableAccounts( - ctx context.Context, - dbTx database.Transaction, -) ([]*types.AccountIdentifier, []*types.AccountIdentifier, error) { - accounts, err := w.helper.AllAccounts(ctx, dbTx) +// RandomNumberWorker generates a random number between minimum and maximum. +func RandomNumberWorker(rawInput string) (string, error) { + var input job.RandomNumberInput + err := job.UnmarshalInput([]byte(rawInput), &input) if err != nil { - return nil, nil, fmt.Errorf( - "unable to get all accounts: %w", - err, - ) - } - - // If there are no accounts, we should create one. - if len(accounts) == 0 { - return nil, nil, ErrCreateAccount + return "", fmt.Errorf("failed to unmarshal input: %w", err) } - // We fetch all locked accounts to subtract them from AllAccounts. - // We consider an account "locked" if it is actively involved in a broadcast. - unlockedAccounts := []*types.AccountIdentifier{} - lockedAccounts, err := w.helper.LockedAccounts(ctx, dbTx) + result, err := utils.RandomNumber(input.Minimum, input.Maximum) if err != nil { - return nil, nil, fmt.Errorf("unable to get locked accounts: %w", err) + return "", fmt.Errorf("failed to generate random number: %w", err) } - // Convert to a map so can do fast lookups - lockedSet := map[string]struct{}{} - for _, account := range lockedAccounts { - lockedSet[types.Hash(account)] = struct{}{} - } - - for _, account := range accounts { - if _, exists := lockedSet[types.Hash(account)]; !exists { - unlockedAccounts = append(unlockedAccounts, account) - } - } - - return accounts, unlockedAccounts, nil + return marshalString(result), nil } -func shouldCreateRandomAccount( - input *job.FindBalanceInput, - accountCount int, -) (bool, error) { - if input.MinimumBalance.Value != "0" { - return false, nil - } - - if input.CreateLimit <= 0 || accountCount >= input.CreateLimit { - return false, nil - } - - rand, err := utils.RandomNumber( - utils.ZeroInt, - utils.OneHundredInt, - ) +// AssertWorker asserts that a condition is true. +func AssertWorker(rawInput string) error { + var input job.AssertInput + err := job.UnmarshalInput([]byte(rawInput), &input) if err != nil { - return false, fmt.Errorf( - "failed to return random number in [%d-%d]: %w", - utils.ZeroInt, - utils.OneHundredInt, - err, - ) - } - - if rand.Int64() >= int64(input.CreateProbability) { - return false, nil - } - - return true, nil -} - -// findBalanceWorkerInputValidation ensures the input to FindBalanceWorker -// is valid. -func findBalanceWorkerInputValidation(input *job.FindBalanceInput) error { - if err := asserter.Amount(input.MinimumBalance); err != nil { - return fmt.Errorf( - "minimum balance %s is invalid: %w", - types.PrintStruct(input.MinimumBalance), - err, - ) + return fmt.Errorf("failed to unmarshal input: %w", err) } - if input.AccountIdentifier != nil { - if err := asserter.AccountIdentifier(input.AccountIdentifier); err != nil { - return fmt.Errorf( - "account identifier %s is invalid: %w", - types.PrintStruct(input.AccountIdentifier), - err, - ) - } - - if input.SubAccountIdentifier != nil { - return errors.New("cannot populate both account and sub account") - } - - if len(input.NotAccountIdentifier) > 0 { - return errors.New("cannot populate both account and not accounts") - } - - if len(input.NotAddress) > 0 { - return errors.New("cannot populate both account and not address") - } - } - - if len(input.NotAccountIdentifier) > 0 { - if err := asserter.AccountArray("not account identifier", input.NotAccountIdentifier); err != nil { - return fmt.Errorf( - "account identifiers of not account identifier %s are invalid: %w", - types.PrintStruct(input.NotAccountIdentifier), - err, - ) - } + if !input.Condition { + return errors.New(input.Message) } return nil } -func skipAccount(input job.FindBalanceInput, account *types.AccountIdentifier) bool { - // If we require an account and that account - // is not equal to the account we are considering, - // we should continue. - if input.AccountIdentifier != nil && - types.Hash(account) != types.Hash(input.AccountIdentifier) { - return true - } - - // If we specify not to use certain addresses and we are considering - // one of them, we should continue. - if utils.ContainsString(input.NotAddress, account.Address) { - return true - } - - // If we specify that we do not use certain accounts - // and the account we are considering is one of them, - // we should continue. - if utils.ContainsAccountIdentifier(input.NotAccountIdentifier, account) { - return true - } - - // If we require a particular SubAccountIdentifier, we skip - // if the account we are examining does not have it. - if input.SubAccountIdentifier != nil && - (account.SubAccount == nil || - types.Hash(account.SubAccount) != types.Hash(input.SubAccountIdentifier)) { - return true - } - - return false -} - -// FindBalanceWorker attempts to find an account (and coin) with some minimum -// balance in a particular currency. -func (w *Worker) FindBalanceWorker( +// FindCurrencyAmountWorker finds the amount for a specific currency. +func (w *Worker) FindCurrencyAmountWorker( ctx context.Context, dbTx database.Transaction, rawInput string, ) (string, error) { - var input job.FindBalanceInput - err := job.UnmarshalInput([]byte(rawInput), &input) - if err != nil { - return "", fmt.Errorf("failed to unmarshal input %s: %w", rawInput, err) - } - - // Validate that input is properly formatted - if err := findBalanceWorkerInputValidation(&input); err != nil { - return "", fmt.Errorf("failed to validate the input of find balance worker: %w", err) - } - - log.Println(balanceMessage(&input)) - - accounts, availableAccounts, err := w.availableAccounts(ctx, dbTx) - if err != nil { - return "", fmt.Errorf("unable to get available accounts: %w", err) - } - - // Randomly, we choose to generate a new account. If we didn't do this, - // we would never grow past 2 accounts for mocking transfers. - shouldCreate, err := shouldCreateRandomAccount(&input, len(accounts)) - if err != nil { - return "", fmt.Errorf("unable to determine if should create: %w", err) - } - - if shouldCreate { - return "", ErrCreateAccount - } - - var unmatchedAccounts []string - // Consider each available account as a potential account. - for _, account := range availableAccounts { - if skipAccount(input, account) { - continue - } - - var output string - var err error - if input.RequireCoin { - output, err = w.checkAccountCoins(ctx, dbTx, &input, account) - } else { - output, err = w.checkAccountBalance(ctx, dbTx, &input, account) - } - if err != nil { - return "", fmt.Errorf("failed to check account coins or balance: %w", err) - } - - // If we did not fund a match, we should continue. - if len(output) == 0 { - unmatchedAccounts = append(unmatchedAccounts, account.Address) - continue - } - - return output, nil - } - - if len(unmatchedAccounts) > 0 { - log.Printf("%d account(s) insufficiently funded. Please fund the address %+v", - len(unmatchedAccounts), - unmatchedAccounts, - ) - } - - // If we can't do anything, we should return with ErrUnsatisfiable. - if input.MinimumBalance.Value != "0" { - return "", ErrUnsatisfiable - } - - // If we should create an account and the number of accounts - // we have is less than the limit, we return ErrCreateAccount. - if input.CreateLimit > 0 && len(accounts) < input.CreateLimit { - return "", ErrCreateAccount - } - - // If we reach here, it means we shouldn't create another account - // and should just return unsatisfiable. - return "", ErrUnsatisfiable -} - -// AssertWorker checks if an input is < 0. -func AssertWorker(rawInput string) error { - // We unmarshal the input here to handle string - // unwrapping automatically. - var input string - err := job.UnmarshalInput([]byte(rawInput), &input) - if err != nil { - return fmt.Errorf("failed to unmarshal input %s: %w", rawInput, err) - } - - val, err := types.BigInt(input) - if err != nil { - return fmt.Errorf("failed to convert string %s to big int: %w", input, err) - } - - if val.Sign() < 0 { - return fmt.Errorf("%s < 0: %w", val.String(), ErrActionFailed) - } - - return nil -} - -// FindCurrencyAmountWorker finds a *types.Amount with a specific -// *types.Currency in a []*types.Amount. -func FindCurrencyAmountWorker(rawInput string) (string, error) { var input job.FindCurrencyAmountInput err := job.UnmarshalInput([]byte(rawInput), &input) if err != nil { - return "", fmt.Errorf("failed to unmarshal input %s: %w", rawInput, err) + return "", fmt.Errorf("failed to unmarshal input: %w", err) } if err := asserter.Currency(input.Currency); err != nil { - return "", fmt.Errorf("currency %s is invalid: %w", types.PrintStruct(input.Currency), err) - } - - if err := asserter.AssertUniqueAmounts(input.Amounts); err != nil { - return "", fmt.Errorf("amount %s is invalid: %w", types.PrintStruct(input.Amounts), err) - } - - for _, amount := range input.Amounts { - if types.Hash(amount.Currency) != types.Hash(input.Currency) { - continue - } - - return types.PrintStruct(amount), nil + return "", fmt.Errorf("currency is invalid: %w", err) } - return "", fmt.Errorf( - "unable to find currency %s: %w", - types.PrintStruct(input.Currency), - ErrActionFailed, - ) -} - -// LoadEnvWorker loads an environment variable and stores -// it in state. This is useful for algorithmic fauceting. -func LoadEnvWorker(rawInput string) (string, error) { - // We unmarshal the input here to handle string - // unwrapping automatically. - var input string - err := job.UnmarshalInput([]byte(rawInput), &input) + amount, err := w.helper.Balance(ctx, dbTx, input.AccountIdentifier, input.Currency) if err != nil { - return "", fmt.Errorf("failed to unmarshal input %s: %w", rawInput, err) + return "", fmt.Errorf("failed to get balance: %w", err) } - return os.Getenv(input), nil + return types.PrettyPrintStruct(amount) } -// HTTPRequestWorker makes an HTTP request and returns the response to -// store in a variable. This is useful for algorithmic fauceting. -func HTTPRequestWorker(rawInput string) (string, error) { - var input job.HTTPRequestInput - err := job.UnmarshalInput([]byte(rawInput), &input) - if err != nil { - return "", fmt.Errorf("failed to unmarshal input %s: %w", rawInput, err) - } - - if input.Timeout <= 0 { - return "", fmt.Errorf("%d is not a valid timeout: %w", input.Timeout, ErrInvalidInput) - } - - if _, err := url.ParseRequestURI(input.URL); err != nil { - return "", fmt.Errorf("failed to parse request URI %s: %w", input.URL, err) - } - - client := &http.Client{Timeout: time.Duration(input.Timeout) * time.Second} - var request *http.Request - switch input.Method { - case job.MethodGet: - request, err = http.NewRequest(http.MethodGet, input.URL, nil) - if err != nil { - return "", fmt.Errorf("failed to generate new request: %w", err) - } - request.Header.Set("Accept", "application/json") - case job.MethodPost: - request, err = http.NewRequest( - http.MethodPost, - input.URL, - bytes.NewBufferString(input.Body), - ) - if err != nil { - return "", fmt.Errorf("failed to generate new request: %w", err) - } - request.Header.Set("Content-Type", "application/json") - request.Header.Set("Accept", "application/json") - default: - return "", fmt.Errorf( - "%s is not a supported HTTP method: %w", - input.Method, - ErrInvalidInput, - ) - } - - resp, err := client.Do(request) - if err != nil { - return "", fmt.Errorf("failed to send request: %w", err) - } - defer func() { - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - }() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response: %w", err) - } - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf( - "status code %d with body %s: %w", - resp.StatusCode, - body, - ErrActionFailed, - ) - } - - return string(body), nil -} - -// SetBlobWorker transactionally saves a key and value for use -// across workflows. -func (w *Worker) SetBlobWorker( +// BroadcastWorker broadcasts a transaction. +func (w *Worker) BroadcastWorker( ctx context.Context, dbTx database.Transaction, rawInput string, -) error { - var input job.SetBlobInput +) (string, error) { + var input job.BroadcastInput err := job.UnmarshalInput([]byte(rawInput), &input) if err != nil { - return fmt.Errorf("failed to unmarshal input %s: %w", rawInput, err) - } - - // By using interface{} for key, we can ensure that JSON - // objects with the same keys but in a different order are - // treated as equal. - if err := w.helper.SetBlob(ctx, dbTx, types.Hash(input.Key), input.Value); err != nil { - return fmt.Errorf("failed to set blob: %w", err) + return "", fmt.Errorf("failed to unmarshal input: %w", err) } - return nil -} - -// GetBlobWorker transactionally retrieves a value associated with -// a key, if it exists. -func (w *Worker) GetBlobWorker( - ctx context.Context, - dbTx database.Transaction, - rawInput string, -) (string, error) { - var input job.GetBlobInput - err := job.UnmarshalInput([]byte(rawInput), &input) - if err != nil { - return "", fmt.Errorf("failed to unmarshal input %s: %w", rawInput, err) + if err := asserter.NetworkIdentifier(input.NetworkIdentifier); err != nil { + return "", fmt.Errorf("network identifier is invalid: %w", err) } - // By using interface{} for key, we can ensure that JSON - // objects with the same keys but in a different order are - // treated as equal. - exists, val, err := w.helper.GetBlob(ctx, dbTx, types.Hash(input.Key)) + txID, metadata, err := w.helper.Broadcast(ctx, input.NetworkIdentifier, input.SignedTransaction) if err != nil { - return "", fmt.Errorf("failed to get blob: %w", err) + return "", fmt.Errorf("failed to broadcast transaction: %w", err) } - if !exists { - return "", fmt.Errorf( - "key %s does not exist: %w", - types.PrintStruct(input.Key), - ErrActionFailed, - ) + result := &job.BroadcastOutput{ + TransactionIdentifier: txID, + Metadata: metadata, } - return string(val), nil + return types.PrettyPrintStruct(result), nil } From accf1e1818ec71e7acb342b185d80ce1c13dabb3 Mon Sep 17 00:00:00 2001 From: Kubudak90 Date: Wed, 8 Apr 2026 03:04:53 +0300 Subject: [PATCH 2/2] fix: EncodeJSONResponse superfluous WriteHeader call Fixes #407 The EncodeJSONResponse function was calling w.WriteHeader(status) before json.Encode(), which caused a superfluous WriteHeader call when encoding failed and http.Error() tried to write another header. Changes: - Encode JSON to a buffer first before writing headers - Only write status header after successful encoding - Return 500 status on encoding error without calling http.Error This prevents the 'http: superfluous response.WriteHeader call' error that occurred when JSON encoding failed. --- server/routers.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/server/routers.go b/server/routers.go index 559950fec..0e5c29305 100644 --- a/server/routers.go +++ b/server/routers.go @@ -17,6 +17,7 @@ package server import ( + "bytes" "encoding/json" "net/http" @@ -80,9 +81,17 @@ func NewRouter(routers ...Router) http.Handler { // optional status code func EncodeJSONResponse(i interface{}, status int, w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(status) - if err := json.NewEncoder(w).Encode(i); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + // Encode to buffer first to avoid superfluous WriteHeader call on error + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(i); err != nil { + // Log the error but don't call http.Error since headers are already set + // The client will receive an empty body with the original status code + // This is better than causing a panic from superfluous WriteHeader + w.WriteHeader(http.StatusInternalServerError) + return } + + w.WriteHeader(status) + w.Write(buf.Bytes()) }