Skip to content

Commit 1887a1b

Browse files
authored
feat(1340): changes for thrown errors in service layer (#1970)
Signed-off-by: mmyslblocky <michal.myslinski@blockydevs.com>
1 parent a9036f9 commit 1887a1b

32 files changed

Lines changed: 170 additions & 121 deletions

docs/adr/ADR-007-structured-error-handling.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,19 @@ export abstract class CliError extends Error {
124124

125125
### Error Type Hierarchy
126126

127-
| Class | Static `CODE` | Recoverable | Use Case |
128-
| -------------------- | --------------------- | -------------------- | -------------------------------------------------------- |
129-
| `ValidationError` | `VALIDATION_ERROR` | No | Input validation, Zod errors |
130-
| `NotFoundError` | `NOT_FOUND` | No | Entity not found (account, token, alias) |
131-
| `NetworkError` | `NETWORK_ERROR` | Yes | HTTP 5xx, timeouts, connection errors |
132-
| `AuthorizationError` | `AUTHORIZATION_ERROR` | No | Permission denied, invalid keys, HTTP 401/403 |
133-
| `ConfigurationError` | `CONFIGURATION_ERROR` | No | Missing config, invalid settings |
134-
| `TransactionError` | `TRANSACTION_ERROR` | explicit (2nd param) | Hedera transaction failures (BUSY_NETWORK → recoverable) |
135-
| `StateError` | `STATE_ERROR` | No | State corruption, invalid state |
136-
| `FileError` | `FILE_ERROR` | No | File I/O errors |
127+
| Class | Static `CODE` | Recoverable | Use Case |
128+
| ---------------------------- | ------------------------------ | -------------------- | ------------------------------------------------------------------- |
129+
| `ValidationError` | `VALIDATION_ERROR` | No | Input validation, Zod errors |
130+
| `NotFoundError` | `NOT_FOUND` | No | Entity not found (account, token, alias) |
131+
| `NetworkError` | `NETWORK_ERROR` | Yes | HTTP 5xx, timeouts, connection errors |
132+
| `AuthorizationError` | `AUTHORIZATION_ERROR` | No | Permission denied, invalid keys, HTTP 401/403 |
133+
| `ConfigurationError` | `CONFIGURATION_ERROR` | No | Missing config, invalid settings |
134+
| `TransactionError` | `TRANSACTION_ERROR` | explicit (2nd param) | Hedera transaction failures (BUSY_NETWORK → recoverable) |
135+
| `TransactionPrecheckError` | `TRANSACTION_PRECHECK_ERROR` | No | Hedera precheck failures before submission (bad input, wrong payer) |
136+
| `TransactionValidationError` | `TRANSACTION_VALIDATION_ERROR` | No | Invalid parameters detected while building a transaction |
137+
| `StateError` | `STATE_ERROR` | No | State corruption, invalid state |
138+
| `FileError` | `FILE_ERROR` | No | File I/O errors |
139+
| `InternalError` | `INTERNAL_ERROR` | No | Unexpected internal failures; catch-all for unclassified errors |
137140

138141
**Note**: All errors exit with code 1. Scripts should use the JSON `code` field to differentiate error types. Human output uses `CliError.getTemplate()`; by default it renders `{{message}}`.
139142

@@ -497,6 +500,9 @@ src/core/errors/
497500
├── authorization-error.ts
498501
├── configuration-error.ts
499502
├── transaction-error.ts
503+
├── transaction-precheck-error.ts
504+
├── transaction-validation-error.ts
505+
├── internal-error.ts
500506
├── state-error.ts
501507
└── file-error.ts
502508
```

src/core/errors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export { NotFoundError } from './not-found-error';
1212
export { StateError } from './state-error';
1313
export { TransactionError } from './transaction-error';
1414
export { TransactionPrecheckError } from './transaction-precheck-error';
15+
export { TransactionValidationError } from './transaction-validation-error';
1516
export { ValidationError } from './validation-error';
1617
export {
1718
formatZodIssueLine,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CliError } from './cli-error';
2+
3+
export class TransactionValidationError extends CliError {
4+
static readonly CODE = 'TRANSACTION_VALIDATION_ERROR';
5+
6+
constructor(
7+
message: string,
8+
options?: { context?: Record<string, unknown>; cause?: unknown },
9+
) {
10+
super({
11+
code: TransactionValidationError.CODE,
12+
message,
13+
recoverable: false,
14+
...options,
15+
});
16+
}
17+
}

src/core/services/account/__tests__/unit/account-transaction-service.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
MOCK_ACCOUNT_ID,
1414
} from '@/__tests__/mocks/fixtures';
1515
import { makeLogger } from '@/__tests__/mocks/mocks';
16-
import { ValidationError } from '@/core/errors';
16+
import { TransactionValidationError } from '@/core/errors';
1717
import { AccountServiceImpl } from '@/core/services/account/account-transaction-service';
1818

1919
import {
@@ -192,7 +192,7 @@ describe('AccountServiceImpl', () => {
192192
};
193193

194194
expect(() => accountService.createAccount(params)).toThrow(
195-
ValidationError,
195+
TransactionValidationError,
196196
);
197197

198198
HbarMock.fromTinybars.mockReturnValue(mockHbarInstance);

src/core/services/account/account-transaction-service.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
PublicKey,
2424
} from '@hiero-ledger/sdk';
2525

26-
import { ValidationError } from '@/core/errors';
26+
import { TransactionValidationError } from '@/core/errors';
2727

2828
export class AccountServiceImpl implements AccountService {
2929
private logger: Logger;
@@ -63,10 +63,13 @@ export class AccountServiceImpl implements AccountService {
6363
publicKey: params.publicKey,
6464
};
6565
} catch (error) {
66-
throw new ValidationError('Invalid account creation parameters', {
67-
context: { publicKey: params.publicKey, balance: params.balanceRaw },
68-
cause: error,
69-
});
66+
throw new TransactionValidationError(
67+
'Invalid account creation parameters',
68+
{
69+
context: { publicKey: params.publicKey, balance: params.balanceRaw },
70+
cause: error,
71+
},
72+
);
7073
}
7174
}
7275

@@ -82,13 +85,16 @@ export class AccountServiceImpl implements AccountService {
8285

8386
return { transaction };
8487
} catch (error) {
85-
throw new ValidationError('Invalid account delete parameters', {
86-
context: {
87-
accountId: params.accountId,
88-
transferAccountId: params.transferAccountId,
88+
throw new TransactionValidationError(
89+
'Invalid account delete parameters',
90+
{
91+
context: {
92+
accountId: params.accountId,
93+
transferAccountId: params.transferAccountId,
94+
},
95+
cause: error,
8996
},
90-
cause: error,
91-
});
97+
);
9298
}
9399
}
94100

@@ -142,10 +148,13 @@ export class AccountServiceImpl implements AccountService {
142148

143149
return { transaction };
144150
} catch (error) {
145-
throw new ValidationError('Invalid account update parameters', {
146-
context: { accountId: params.accountId },
147-
cause: error,
148-
});
151+
throw new TransactionValidationError(
152+
'Invalid account update parameters',
153+
{
154+
context: { accountId: params.accountId },
155+
cause: error,
156+
},
157+
);
149158
}
150159
}
151160

@@ -162,7 +171,7 @@ export class AccountServiceImpl implements AccountService {
162171
);
163172
return query;
164173
} catch (error) {
165-
throw new ValidationError('Invalid account ID format', {
174+
throw new TransactionValidationError('Invalid account ID format', {
166175
context: { accountId },
167176
cause: error,
168177
});

src/core/services/alias/__tests__/unit/alias-service.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Logger } from '@/core/services/logger/logger-service.interface';
77
import type { StateService } from '@/core/services/state/state-service.interface';
88

99
import { makeLogger, makeStateMock } from '@/__tests__/mocks/mocks';
10-
import { NotFoundError, ValidationError } from '@/core/errors';
10+
import { NotFoundError, StateError, ValidationError } from '@/core/errors';
1111
import { AliasServiceImpl } from '@/core/services/alias/alias-service';
1212
import { AliasType, SupportedNetwork } from '@/core/types/shared.types';
1313

@@ -65,7 +65,7 @@ describe('AliasServiceImpl', () => {
6565

6666
const record = createAliasRecord();
6767

68-
expect(() => aliasService.register(record)).toThrow(ValidationError);
68+
expect(() => aliasService.register(record)).toThrow(StateError);
6969
expect(stateMock.set).not.toHaveBeenCalled();
7070
});
7171

src/core/services/alias/alias-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { StateService } from '@/core/services/state/state-service.interface
33
import type { AliasType, SupportedNetwork } from '@/core/types/shared.types';
44
import type { AliasRecord, AliasService } from './alias-service.interface';
55

6-
import { NotFoundError, ValidationError } from '@/core/errors';
6+
import { NotFoundError, StateError, ValidationError } from '@/core/errors';
77
import { composeKey } from '@/core/utils/key-composer';
88

99
const NAMESPACE = 'aliases';
@@ -19,7 +19,7 @@ export class AliasServiceImpl implements AliasService {
1919

2020
register(record: AliasRecord): void {
2121
if (this.exists(record.alias, record.network)) {
22-
throw new ValidationError(
22+
throw new StateError(
2323
`Alias already exists for network=${record.network}: ${record.alias}`,
2424
{ context: { alias: record.alias, network: record.network } },
2525
);

src/core/services/allowance/allowance-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
TokenId,
1212
} from '@hiero-ledger/sdk';
1313

14-
import { ValidationError } from '@/core/errors';
14+
import { TransactionValidationError, ValidationError } from '@/core/errors';
1515
import { HEDERA_MAX_ALLOWANCE_ENTRIES_PER_TRANSACTION } from '@/core/shared/constants';
1616

1717
export class AllowanceServiceImpl implements AllowanceService {
@@ -24,7 +24,7 @@ export class AllowanceServiceImpl implements AllowanceService {
2424
);
2525
}
2626
if (entries.length > HEDERA_MAX_ALLOWANCE_ENTRIES_PER_TRANSACTION) {
27-
throw new ValidationError(
27+
throw new TransactionValidationError(
2828
`AccountAllowanceApproveTransaction supports at most ${HEDERA_MAX_ALLOWANCE_ENTRIES_PER_TRANSACTION} entries per transaction`,
2929
);
3030
}

src/core/services/config/__tests__/unit/config-service.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import type { StateService } from '@/core/services/state/state-service.interface';
66

77
import { makeStateMock } from '@/__tests__/mocks/mocks';
8-
import { ValidationError } from '@/core/errors';
8+
import { TransactionValidationError } from '@/core/errors';
99
import { ConfigServiceImpl } from '@/core/services/config/config-service';
1010
import { ConfigOptionKey } from '@/core/services/config/config-service.interface';
1111
import { KeyManager } from '@/core/services/kms/kms-types.interface';
@@ -116,7 +116,7 @@ describe('ConfigServiceImpl', () => {
116116

117117
it('should throw error for unknown option', () => {
118118
expect(() => configService.getOption('unknown_option')).toThrow(
119-
ValidationError,
119+
TransactionValidationError,
120120
);
121121
});
122122

@@ -158,14 +158,14 @@ describe('ConfigServiceImpl', () => {
158158

159159
it('should throw error for unknown option', () => {
160160
expect(() => configService.setOption('unknown_option', 'value')).toThrow(
161-
ValidationError,
161+
TransactionValidationError,
162162
);
163163
});
164164

165165
it('should throw error when setting non-boolean for boolean option', () => {
166166
expect(() =>
167167
configService.setOption(ConfigOptionKey.ed25519_support, 'not_boolean'),
168-
).toThrow(ValidationError);
168+
).toThrow(TransactionValidationError);
169169
});
170170

171171
it('should set enum option with valid value', () => {
@@ -181,13 +181,13 @@ describe('ConfigServiceImpl', () => {
181181
it('should throw error for invalid enum value', () => {
182182
expect(() =>
183183
configService.setOption(ConfigOptionKey.log_level, 'invalid'),
184-
).toThrow(ValidationError);
184+
).toThrow(TransactionValidationError);
185185
});
186186

187187
it('should throw error when setting non-string for enum option', () => {
188188
expect(() =>
189189
configService.setOption(ConfigOptionKey.log_level, 123),
190-
).toThrow(ValidationError);
190+
).toThrow(TransactionValidationError);
191191
});
192192

193193
it('should set default_key_manager enum option', () => {

src/core/services/config/config-service.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
ConfigService,
55
} from './config-service.interface';
66

7-
import { InternalError, ValidationError } from '@/core/errors';
7+
import { TransactionValidationError } from '@/core/errors';
88
import { isStringifiable } from '@/core/utils/is-stringifiable';
99

1010
import { CONFIG_NAMESPACE, CONFIG_OPTIONS } from './config-service.interface';
@@ -40,7 +40,7 @@ export class ConfigServiceImpl implements ConfigService {
4040
getOption<T = boolean | number | string>(name: string): T {
4141
const spec = CONFIG_OPTIONS[name];
4242
if (!spec) {
43-
throw new ValidationError(`Unknown config option: ${name}`, {
43+
throw new TransactionValidationError(`Unknown config option: ${name}`, {
4444
context: { optionName: name },
4545
});
4646
}
@@ -83,14 +83,14 @@ export class ConfigServiceImpl implements ConfigService {
8383
setOption(name: string, value: boolean | number | string): void {
8484
const spec = CONFIG_OPTIONS[name];
8585
if (!spec) {
86-
throw new ValidationError(`Unknown config option: ${name}`, {
86+
throw new TransactionValidationError(`Unknown config option: ${name}`, {
8787
context: { optionName: name },
8888
});
8989
}
9090
switch (spec.type) {
9191
case 'boolean':
9292
if (typeof value !== 'boolean') {
93-
throw new ValidationError(
93+
throw new TransactionValidationError(
9494
`Invalid value for ${name}: expected boolean`,
9595
{
9696
context: { optionName: name, value, expectedType: 'boolean' },
@@ -101,7 +101,7 @@ export class ConfigServiceImpl implements ConfigService {
101101
return;
102102
case 'number': {
103103
if (typeof value !== 'number' || Number.isNaN(value)) {
104-
throw new ValidationError(
104+
throw new TransactionValidationError(
105105
`Invalid value for ${name}: expected number`,
106106
{
107107
context: { optionName: name, value, expectedType: 'number' },
@@ -113,7 +113,7 @@ export class ConfigServiceImpl implements ConfigService {
113113
}
114114
case 'string': {
115115
if (typeof value !== 'string') {
116-
throw new ValidationError(
116+
throw new TransactionValidationError(
117117
`Invalid value for ${name}: expected string`,
118118
{
119119
context: { optionName: name, value, expectedType: 'string' },
@@ -126,7 +126,7 @@ export class ConfigServiceImpl implements ConfigService {
126126
case 'enum': {
127127
if (typeof value !== 'string' || !spec.allowedValues.includes(value)) {
128128
const allowed = spec.allowedValues.join(', ');
129-
throw new ValidationError(
129+
throw new TransactionValidationError(
130130
`Invalid value for ${name}: expected one of (${allowed})`,
131131
{
132132
context: {
@@ -142,9 +142,12 @@ export class ConfigServiceImpl implements ConfigService {
142142
return;
143143
}
144144
default:
145-
throw new InternalError(`Unsupported option type for ${name}`, {
146-
context: { optionName: name },
147-
});
145+
throw new TransactionValidationError(
146+
`Unsupported option type for ${name}`,
147+
{
148+
context: { optionName: name },
149+
},
150+
);
148151
}
149152
}
150153
}

0 commit comments

Comments
 (0)