Skip to content

Commit 66411ac

Browse files
feat(sdk-core): add suspicious transaction error classes
- Add base SuspiciousTransactionError class extending BitGoJsError - Add SuspiciousTransactionParameterMismatchError for recipient mismatches - Add SuspiciousContractInteractionError for contract interaction issues - Add SuspiciousUnauthorizedTokenApproval for unauthorized token approvals - Add complete unit test suite with 7 test cases covering all error scenarios TICKET: WP-6187
1 parent b25fe3f commit 66411ac

File tree

2 files changed

+355
-1
lines changed

2 files changed

+355
-1
lines changed

modules/sdk-core/src/bitgo/errors.ts

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Descriptive error types for common issues which may arise
22
// during the operation of BitGoJS or BitGoExpress
33
import { BitGoJsError } from '../bitgojsError';
4+
import { IRequestTracer } from '../api/types';
5+
import { TransactionParams } from './baseCoin';
6+
import { TokenTransferRecipientParams } from './utils/tss/baseTypes';
47

58
// re-export for backwards compat
69
export { BitGoJsError };
@@ -175,7 +178,7 @@ export class NeedUserSignupError extends BitGoJsError {
175178
}
176179
}
177180

178-
export class ApiResponseError<ResponseBodyType = any> extends BitGoJsError {
181+
export class ApiResponseError<ResponseBodyType = unknown> extends BitGoJsError {
179182
message: string;
180183
status: number;
181184
result?: ResponseBodyType;
@@ -197,3 +200,188 @@ export class ApiResponseError<ResponseBodyType = any> extends BitGoJsError {
197200
this.needsOTP = needsOTP;
198201
}
199202
}
203+
204+
/**
205+
* Interface for token approval information used in suspicious transaction detection
206+
*
207+
* @interface TokenApproval
208+
* @property {string} [tokenName] - Optional human-readable name of the token
209+
* @property {string} tokenAddress - The contract address of the token being approved
210+
* @property {number | 'unlimited'} authorizingAmount - The amount being authorized for spending, or 'unlimited' for infinite approval
211+
* @property {string} authorizingAddress - The address being authorized to spend the tokens
212+
*/
213+
export interface TokenApproval {
214+
tokenName?: string;
215+
tokenAddress: string;
216+
authorizingAmount: number | 'unlimited';
217+
authorizingAddress: string;
218+
}
219+
220+
/**
221+
* Interface for mismatched recipient information detected during transaction verification
222+
*
223+
* @interface MismatchedRecipient
224+
* @property {string} address - The recipient address that was found in the transaction
225+
* @property {string} amount - The amount being sent to this recipient
226+
* @property {string | TokenTransferRecipientParams} [data] - Optional transaction data or token transfer parameters
227+
* @property {string} [tokenName] - Optional name of the token being transferred
228+
* @property {TokenTransferRecipientParams} [tokenData] - Optional structured token transfer data
229+
*/
230+
export interface MismatchedRecipient {
231+
address: string;
232+
amount: string;
233+
data?: string | TokenTransferRecipientParams;
234+
tokenName?: string;
235+
tokenData?: TokenTransferRecipientParams;
236+
}
237+
238+
/**
239+
* Interface for contract interaction data payload used in suspicious transaction detection
240+
*
241+
* @interface ContractDataPayload
242+
* @property {string} address - The contract address being interacted with
243+
* @property {string} rawContractPayload - The raw contract payload in serialized form specific to the blockchain
244+
* @property {unknown} decodedContractPayload - The decoded contract payload, structure varies by coin/chain implementation
245+
*/
246+
export interface ContractDataPayload {
247+
address: string;
248+
// The raw contract payload in serialized form of the chain
249+
rawContractPayload: string;
250+
// To be defined on a per-coin basis
251+
decodedContractPayload: unknown;
252+
}
253+
254+
/**
255+
* Base error class for suspicious transaction detection
256+
*
257+
* This error is thrown when a transaction is detected to have suspicious characteristics
258+
* that don't match the expected parameters or behavior.
259+
*
260+
* @class SuspiciousTransactionError
261+
* @extends {BitGoJsError}
262+
* @property {string | IRequestTracer} id - Transaction ID or request tracer for tracking
263+
* @property {TransactionParams[]} txParams - Array of transaction parameters that were analyzed
264+
* @property {string} txHex - The raw transaction in hexadecimal format
265+
*/
266+
export class SuspiciousTransactionError extends BitGoJsError {
267+
public readonly id: string | IRequestTracer;
268+
public readonly txParams: TransactionParams[];
269+
public readonly txHex: string;
270+
271+
/**
272+
* Creates an instance of SuspiciousTransactionError
273+
*
274+
* @param {string} message - Error message describing the suspicious activity
275+
* @param {string | IRequestTracer} id - Transaction ID or request tracer
276+
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
277+
* @param {string} txHex - Raw transaction hex string
278+
*/
279+
public constructor(message: string, id: string | IRequestTracer, txParams: TransactionParams[], txHex: string) {
280+
super(message);
281+
this.id = id;
282+
this.txParams = txParams;
283+
this.txHex = txHex;
284+
}
285+
}
286+
287+
/**
288+
* Error thrown when transaction parameters don't match the expected recipients
289+
*
290+
* This error occurs when the transaction contains recipients or amounts that differ
291+
* from what was expected based on the original transaction parameters.
292+
*
293+
* @class SuspiciousTransactionParameterMismatchError
294+
* @extends {SuspiciousTransactionError}
295+
* @property {MismatchedRecipient[]} mismatchedRecipients - Array of recipients that don't match expectations
296+
*/
297+
export class SuspiciousTransactionParameterMismatchError extends SuspiciousTransactionError {
298+
public readonly mismatchedRecipients: MismatchedRecipient[];
299+
300+
/**
301+
* Creates an instance of SuspiciousTransactionParameterMismatchError
302+
*
303+
* @param {string} message - Error message describing the parameter mismatch
304+
* @param {string | IRequestTracer} id - Transaction ID or request tracer
305+
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
306+
* @param {string} txHex - Raw transaction hex string
307+
* @param {MismatchedRecipient[]} mismatchedRecipients - Array of mismatched recipient information
308+
*/
309+
public constructor(
310+
message: string,
311+
id: string | IRequestTracer,
312+
txParams: TransactionParams[],
313+
txHex: string,
314+
mismatchedRecipients: MismatchedRecipient[]
315+
) {
316+
super(message, id, txParams, txHex);
317+
this.mismatchedRecipients = mismatchedRecipients;
318+
}
319+
}
320+
321+
/**
322+
* Error thrown when contract interaction data doesn't match expected payload
323+
*
324+
* This error occurs when a transaction interacts with a smart contract but the
325+
* contract call data or method doesn't match what was expected.
326+
*
327+
* @class SuspiciousContractInteractionError
328+
* @extends {SuspiciousTransactionError}
329+
* @property {ContractDataPayload} mismatchedDataPayload - The contract interaction data that was unexpected
330+
*/
331+
export class SuspiciousContractInteractionError extends SuspiciousTransactionError {
332+
public readonly mismatchedDataPayload: ContractDataPayload;
333+
334+
/**
335+
* Creates an instance of SuspiciousContractInteractionError
336+
*
337+
* @param {string} message - Error message describing the contract interaction issue
338+
* @param {string | IRequestTracer} id - Transaction ID or request tracer
339+
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
340+
* @param {string} txHex - Raw transaction hex string
341+
* @param {ContractDataPayload} mismatchedDataPayload - The unexpected contract interaction data
342+
*/
343+
public constructor(
344+
message: string,
345+
id: string | IRequestTracer,
346+
txParams: TransactionParams[],
347+
txHex: string,
348+
mismatchedDataPayload: ContractDataPayload
349+
) {
350+
super(message, id, txParams, txHex);
351+
this.mismatchedDataPayload = mismatchedDataPayload;
352+
}
353+
}
354+
355+
/**
356+
* Error thrown when unauthorized token approval is detected
357+
*
358+
* This error occurs when a transaction contains a token approval that wasn't
359+
* expected or authorized by the user, potentially indicating malicious activity.
360+
*
361+
* @class SuspiciousUnauthorizedTokenApproval
362+
* @extends {SuspiciousTransactionError}
363+
* @property {TokenApproval} tokenApproval - Details of the unauthorized token approval
364+
*/
365+
export class SuspiciousUnauthorizedTokenApproval extends SuspiciousTransactionError {
366+
public readonly tokenApproval: TokenApproval;
367+
368+
/**
369+
* Creates an instance of SuspiciousUnauthorizedTokenApproval
370+
*
371+
* @param {string} message - Error message describing the unauthorized approval
372+
* @param {string | IRequestTracer} id - Transaction ID or request tracer
373+
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
374+
* @param {string} txHex - Raw transaction hex string
375+
* @param {TokenApproval} tokenApproval - Details of the unauthorized token approval
376+
*/
377+
public constructor(
378+
message: string,
379+
id: string | IRequestTracer,
380+
txParams: TransactionParams[],
381+
txHex: string,
382+
tokenApproval: TokenApproval
383+
) {
384+
super(message, id, txParams, txHex);
385+
this.tokenApproval = tokenApproval;
386+
}
387+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import should from 'should';
2+
import {
3+
SuspiciousTransactionError,
4+
SuspiciousTransactionParameterMismatchError,
5+
SuspiciousContractInteractionError,
6+
SuspiciousUnauthorizedTokenApproval,
7+
MismatchedRecipient,
8+
ContractDataPayload,
9+
TokenApproval,
10+
} from '../../../src/bitgo/errors';
11+
12+
describe('Suspicious Transaction Errors', () => {
13+
const mockTransactionId = '0x1234567890abcdef';
14+
const mockTxParams: any[] = [
15+
{ address: '0xrecipient1', amount: '1000000000000000000' },
16+
{ address: '0xrecipient2', amount: '2000000000000000000' },
17+
];
18+
const mockTxHex = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
19+
20+
describe('SuspiciousTransactionError', () => {
21+
it('should create base suspicious transaction error with all required properties', () => {
22+
const message = 'Suspicious transaction detected';
23+
const error = new SuspiciousTransactionError(message, mockTransactionId, mockTxParams, mockTxHex);
24+
25+
should.exist(error);
26+
should.equal(error.message, message);
27+
should.equal(error.name, 'SuspiciousTransactionError');
28+
should.equal(error.id, mockTransactionId);
29+
should.deepEqual(error.txParams, mockTxParams);
30+
should.equal(error.txHex, mockTxHex);
31+
});
32+
33+
it('should be an instance of Error', () => {
34+
const error = new SuspiciousTransactionError('Test message', mockTransactionId, mockTxParams, mockTxHex);
35+
36+
should(error).be.instanceOf(Error);
37+
});
38+
});
39+
40+
describe('SuspiciousTransactionParameterMismatchError', () => {
41+
it('should create parameter mismatch error with mismatched recipients', () => {
42+
const message = 'Transaction parameters do not match expected recipients';
43+
const mismatchedRecipients: MismatchedRecipient[] = [
44+
{ address: '0xexpected1', amount: '1000' },
45+
{ address: '0xexpected2', amount: '2000' },
46+
];
47+
48+
const error = new SuspiciousTransactionParameterMismatchError(
49+
message,
50+
mockTransactionId,
51+
mockTxParams,
52+
mockTxHex,
53+
mismatchedRecipients
54+
);
55+
56+
should.exist(error);
57+
should.equal(error.message, message);
58+
should.equal(error.name, 'SuspiciousTransactionParameterMismatchError');
59+
should.equal(error.id, mockTransactionId);
60+
should.deepEqual(error.txParams, mockTxParams);
61+
should.equal(error.txHex, mockTxHex);
62+
should.deepEqual(error.mismatchedRecipients, mismatchedRecipients);
63+
});
64+
});
65+
66+
describe('SuspiciousContractInteractionError', () => {
67+
it('should create contract interaction error with mismatched data payload', () => {
68+
const message = 'Contract interaction data does not match expected payload';
69+
const mismatchedDataPayload: ContractDataPayload = {
70+
address: '0xcontract123',
71+
rawContractPayload: '0xabcdef',
72+
decodedContractPayload: { method: 'transfer', params: ['0xrecipient', '1000'] },
73+
};
74+
75+
const error = new SuspiciousContractInteractionError(
76+
message,
77+
mockTransactionId,
78+
mockTxParams,
79+
mockTxHex,
80+
mismatchedDataPayload
81+
);
82+
83+
should.exist(error);
84+
should.equal(error.message, message);
85+
should.equal(error.name, 'SuspiciousContractInteractionError');
86+
should.equal(error.id, mockTransactionId);
87+
should.deepEqual(error.txParams, mockTxParams);
88+
should.equal(error.txHex, mockTxHex);
89+
should.deepEqual(error.mismatchedDataPayload, mismatchedDataPayload);
90+
});
91+
});
92+
93+
describe('SuspiciousUnauthorizedTokenApproval', () => {
94+
it('should create unauthorized token approval error with token approval details', () => {
95+
const message = 'Unauthorized token approval detected';
96+
const tokenApproval: TokenApproval = {
97+
tokenName: 'TestToken',
98+
tokenAddress: '0xtoken123',
99+
authorizingAmount: 'unlimited',
100+
authorizingAddress: '0xspender456',
101+
};
102+
103+
const error = new SuspiciousUnauthorizedTokenApproval(
104+
message,
105+
mockTransactionId,
106+
mockTxParams,
107+
mockTxHex,
108+
tokenApproval
109+
);
110+
111+
should.exist(error);
112+
should.equal(error.message, message);
113+
should.equal(error.name, 'SuspiciousUnauthorizedTokenApproval');
114+
should.equal(error.id, mockTransactionId);
115+
should.deepEqual(error.txParams, mockTxParams);
116+
should.equal(error.txHex, mockTxHex);
117+
should.deepEqual(error.tokenApproval, tokenApproval);
118+
});
119+
});
120+
121+
describe('Error inheritance and properties', () => {
122+
it('should maintain proper inheritance chain', () => {
123+
const baseError = new SuspiciousTransactionError('Base error', mockTransactionId, mockTxParams, mockTxHex);
124+
const paramError = new SuspiciousTransactionParameterMismatchError(
125+
'Param error',
126+
mockTransactionId,
127+
mockTxParams,
128+
mockTxHex,
129+
[]
130+
);
131+
const contractError = new SuspiciousContractInteractionError(
132+
'Contract error',
133+
mockTransactionId,
134+
mockTxParams,
135+
mockTxHex,
136+
{ address: '0xtest', rawContractPayload: '0x', decodedContractPayload: {} }
137+
);
138+
const tokenError = new SuspiciousUnauthorizedTokenApproval(
139+
'Token error',
140+
mockTransactionId,
141+
mockTxParams,
142+
mockTxHex,
143+
{ tokenAddress: '0xtoken', authorizingAmount: 1000, authorizingAddress: '0xspender' }
144+
);
145+
146+
// All should be instances of Error
147+
should(baseError).be.instanceOf(Error);
148+
should(paramError).be.instanceOf(Error);
149+
should(contractError).be.instanceOf(Error);
150+
should(tokenError).be.instanceOf(Error);
151+
152+
// All should be instances of SuspiciousTransactionError
153+
should(paramError).be.instanceOf(SuspiciousTransactionError);
154+
should(contractError).be.instanceOf(SuspiciousTransactionError);
155+
should(tokenError).be.instanceOf(SuspiciousTransactionError);
156+
});
157+
158+
it('should preserve stack trace', () => {
159+
const error = new SuspiciousTransactionError('Test error', mockTransactionId, mockTxParams, mockTxHex);
160+
161+
should.exist(error.stack);
162+
should(error.stack).be.a.String();
163+
should(error.stack).containEql('SuspiciousTransactionError');
164+
});
165+
});
166+
});

0 commit comments

Comments
 (0)