Skip to content

Commit 6c5dacf

Browse files
feat(sdk-core): add transaction intent mismatch errors
- Add base TxIntentMismatchError class extending BitGoJsError - Add TxIntentMismatchRecipientError for recipient intent mismatches - Add TxIntentMismatchContractError for contract interaction intent mismatches - Add TxIntentMismatchApprovalError for token approval intent mismatches TICKET: WP-6187
1 parent b25fe3f commit 6c5dacf

File tree

2 files changed

+377
-0
lines changed

2 files changed

+377
-0
lines changed

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

Lines changed: 182 additions & 0 deletions
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 { SendManyOptions } from './wallet';
47

58
// re-export for backwards compat
69
export { BitGoJsError };
@@ -197,3 +200,182 @@ 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: { type: 'unlimited' } | { type: 'limited'; amount: number };
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 type MismatchedRecipient = NonNullable<SendManyOptions['recipients']>[0];
231+
232+
/**
233+
* Interface for contract interaction data payload used in suspicious transaction detection
234+
*
235+
* @interface ContractDataPayload
236+
* @property {string} address - The contract address being interacted with
237+
* @property {string} rawContractPayload - The raw contract payload in serialized form specific to the blockchain
238+
* @property {unknown} decodedContractPayload - The decoded contract payload, structure varies by coin/chain implementation
239+
*/
240+
export interface ContractDataPayload {
241+
address: string;
242+
// The raw contract payload in serialized form of the chain
243+
rawContractPayload: string;
244+
// To be defined on a per-coin basis
245+
decodedContractPayload: unknown;
246+
}
247+
248+
/**
249+
* Base error class for transaction intent mismatch detection
250+
*
251+
* This error is thrown when a transaction does not match the user's original intent,
252+
* indicating potential security issues or malicious modifications.
253+
*
254+
* @class TxIntentMismatchError
255+
* @extends {BitGoJsError}
256+
* @property {string | IRequestTracer} id - Transaction ID or request tracer for tracking
257+
* @property {TransactionParams[]} txParams - Array of transaction parameters that were analyzed
258+
* @property {string} txHex - The raw transaction in hexadecimal format
259+
*/
260+
export class TxIntentMismatchError extends BitGoJsError {
261+
public readonly id: string | IRequestTracer;
262+
public readonly txParams: TransactionParams[];
263+
public readonly txHex: string;
264+
265+
/**
266+
* Creates an instance of TxIntentMismatchError
267+
*
268+
* @param {string} message - Error message describing the intent mismatch
269+
* @param {string | IRequestTracer} id - Transaction ID or request tracer
270+
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
271+
* @param {string} txHex - Raw transaction hex string
272+
*/
273+
public constructor(message: string, id: string | IRequestTracer, txParams: TransactionParams[], txHex: string) {
274+
super(message);
275+
this.id = id;
276+
this.txParams = txParams;
277+
this.txHex = txHex;
278+
}
279+
}
280+
281+
/**
282+
* Error thrown when transaction recipients don't match the user's intent
283+
*
284+
* This error occurs when the transaction contains recipients or amounts that differ
285+
* from what the user originally intended to send.
286+
*
287+
* @class TxIntentMismatchRecipientError
288+
* @extends {TxIntentMismatchError}
289+
* @property {MismatchedRecipient[]} mismatchedRecipients - Array of recipients that don't match user intent
290+
*/
291+
export class TxIntentMismatchRecipientError extends TxIntentMismatchError {
292+
public readonly mismatchedRecipients: MismatchedRecipient[];
293+
294+
/**
295+
* Creates an instance of TxIntentMismatchRecipientError
296+
*
297+
* @param {string} message - Error message describing the recipient intent mismatch
298+
* @param {string | IRequestTracer} id - Transaction ID or request tracer
299+
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
300+
* @param {string} txHex - Raw transaction hex string
301+
* @param {MismatchedRecipient[]} mismatchedRecipients - Array of recipients that don't match user intent
302+
*/
303+
public constructor(
304+
message: string,
305+
id: string | IRequestTracer,
306+
txParams: TransactionParams[],
307+
txHex: string,
308+
mismatchedRecipients: MismatchedRecipient[]
309+
) {
310+
super(message, id, txParams, txHex);
311+
this.mismatchedRecipients = mismatchedRecipients;
312+
}
313+
}
314+
315+
/**
316+
* Error thrown when contract interaction doesn't match the user's intent
317+
*
318+
* This error occurs when a transaction interacts with a smart contract but the
319+
* contract call data or method doesn't match what the user intended.
320+
*
321+
* @class TxIntentMismatchContractError
322+
* @extends {TxIntentMismatchError}
323+
* @property {ContractDataPayload} mismatchedDataPayload - The contract interaction data that doesn't match user intent
324+
*/
325+
export class TxIntentMismatchContractError extends TxIntentMismatchError {
326+
public readonly mismatchedDataPayload: ContractDataPayload;
327+
328+
/**
329+
* Creates an instance of TxIntentMismatchContractError
330+
*
331+
* @param {string} message - Error message describing the contract intent mismatch
332+
* @param {string | IRequestTracer} id - Transaction ID or request tracer
333+
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
334+
* @param {string} txHex - Raw transaction hex string
335+
* @param {ContractDataPayload} mismatchedDataPayload - The contract interaction data that doesn't match user intent
336+
*/
337+
public constructor(
338+
message: string,
339+
id: string | IRequestTracer,
340+
txParams: TransactionParams[],
341+
txHex: string,
342+
mismatchedDataPayload: ContractDataPayload
343+
) {
344+
super(message, id, txParams, txHex);
345+
this.mismatchedDataPayload = mismatchedDataPayload;
346+
}
347+
}
348+
349+
/**
350+
* Error thrown when token approval doesn't match the user's intent
351+
*
352+
* This error occurs when a transaction contains a token approval that the user
353+
* did not intend to authorize, potentially indicating malicious activity.
354+
*
355+
* @class TxIntentMismatchApprovalError
356+
* @extends {TxIntentMismatchError}
357+
* @property {TokenApproval} tokenApproval - Details of the token approval that doesn't match user intent
358+
*/
359+
export class TxIntentMismatchApprovalError extends TxIntentMismatchError {
360+
public readonly tokenApproval: TokenApproval;
361+
362+
/**
363+
* Creates an instance of TxIntentMismatchApprovalError
364+
*
365+
* @param {string} message - Error message describing the approval intent mismatch
366+
* @param {string | IRequestTracer} id - Transaction ID or request tracer
367+
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
368+
* @param {string} txHex - Raw transaction hex string
369+
* @param {TokenApproval} tokenApproval - Details of the token approval that doesn't match user intent
370+
*/
371+
public constructor(
372+
message: string,
373+
id: string | IRequestTracer,
374+
txParams: TransactionParams[],
375+
txHex: string,
376+
tokenApproval: TokenApproval
377+
) {
378+
super(message, id, txParams, txHex);
379+
this.tokenApproval = tokenApproval;
380+
}
381+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import should from 'should';
2+
import {
3+
TxIntentMismatchError,
4+
TxIntentMismatchRecipientError,
5+
TxIntentMismatchContractError,
6+
TxIntentMismatchApprovalError,
7+
MismatchedRecipient,
8+
ContractDataPayload,
9+
TokenApproval,
10+
} from '../../../src/bitgo/errors';
11+
12+
describe('Transaction Intent Mismatch 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('TxIntentMismatchError', () => {
21+
it('should create base transaction intent mismatch error with all required properties', () => {
22+
const message = 'Transaction does not match user intent';
23+
const error = new TxIntentMismatchError(message, mockTransactionId, mockTxParams, mockTxHex);
24+
25+
should.exist(error);
26+
should.equal(error.message, message);
27+
should.equal(error.name, 'TxIntentMismatchError');
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 TxIntentMismatchError('Test message', mockTransactionId, mockTxParams, mockTxHex);
35+
36+
should(error).be.instanceOf(Error);
37+
});
38+
});
39+
40+
describe('TxIntentMismatchRecipientError', () => {
41+
it('should create recipient intent mismatch error with mismatched recipients', () => {
42+
const message = 'Transaction recipients do not match user intent';
43+
const mismatchedRecipients: MismatchedRecipient[] = [
44+
{ address: '0xexpected1', amount: '1000' },
45+
{ address: '0xexpected2', amount: '2000' },
46+
];
47+
48+
const error = new TxIntentMismatchRecipientError(
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, 'TxIntentMismatchRecipientError');
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+
it('should be an instance of TxIntentMismatchError', () => {
66+
const error = new TxIntentMismatchRecipientError('Test message', mockTransactionId, mockTxParams, mockTxHex, []);
67+
68+
should(error).be.instanceOf(TxIntentMismatchError);
69+
should(error).be.instanceOf(Error);
70+
});
71+
});
72+
73+
describe('TxIntentMismatchContractError', () => {
74+
it('should create contract intent mismatch error with mismatched data payload', () => {
75+
const message = 'Contract interaction does not match user intent';
76+
const mismatchedDataPayload: ContractDataPayload = {
77+
address: '0xcontract123',
78+
rawContractPayload: '0xabcdef',
79+
decodedContractPayload: { method: 'transfer', params: ['0xrecipient', '1000'] },
80+
};
81+
82+
const error = new TxIntentMismatchContractError(
83+
message,
84+
mockTransactionId,
85+
mockTxParams,
86+
mockTxHex,
87+
mismatchedDataPayload
88+
);
89+
90+
should.exist(error);
91+
should.equal(error.message, message);
92+
should.equal(error.name, 'TxIntentMismatchContractError');
93+
should.equal(error.id, mockTransactionId);
94+
should.deepEqual(error.txParams, mockTxParams);
95+
should.equal(error.txHex, mockTxHex);
96+
should.deepEqual(error.mismatchedDataPayload, mismatchedDataPayload);
97+
});
98+
99+
it('should be an instance of TxIntentMismatchError', () => {
100+
const error = new TxIntentMismatchContractError('Test message', mockTransactionId, mockTxParams, mockTxHex, {
101+
address: '0xtest',
102+
rawContractPayload: '0x',
103+
decodedContractPayload: {},
104+
});
105+
106+
should(error).be.instanceOf(TxIntentMismatchError);
107+
should(error).be.instanceOf(Error);
108+
});
109+
});
110+
111+
describe('TxIntentMismatchApprovalError', () => {
112+
it('should create approval intent mismatch error with token approval details', () => {
113+
const message = 'Token approval does not match user intent';
114+
const tokenApproval: TokenApproval = {
115+
tokenName: 'TestToken',
116+
tokenAddress: '0xtoken123',
117+
authorizingAmount: 'unlimited',
118+
authorizingAddress: '0xspender456',
119+
};
120+
121+
const error = new TxIntentMismatchApprovalError(
122+
message,
123+
mockTransactionId,
124+
mockTxParams,
125+
mockTxHex,
126+
tokenApproval
127+
);
128+
129+
should.exist(error);
130+
should.equal(error.message, message);
131+
should.equal(error.name, 'TxIntentMismatchApprovalError');
132+
should.equal(error.id, mockTransactionId);
133+
should.deepEqual(error.txParams, mockTxParams);
134+
should.equal(error.txHex, mockTxHex);
135+
should.deepEqual(error.tokenApproval, tokenApproval);
136+
});
137+
138+
it('should be an instance of TxIntentMismatchError', () => {
139+
const error = new TxIntentMismatchApprovalError('Test message', mockTransactionId, mockTxParams, mockTxHex, {
140+
tokenAddress: '0xtoken',
141+
authorizingAmount: 1000,
142+
authorizingAddress: '0xspender',
143+
});
144+
145+
should(error).be.instanceOf(TxIntentMismatchError);
146+
should(error).be.instanceOf(Error);
147+
});
148+
});
149+
150+
describe('Error inheritance and properties', () => {
151+
it('should maintain proper inheritance chain', () => {
152+
const baseError = new TxIntentMismatchError('Base error', mockTransactionId, mockTxParams, mockTxHex);
153+
const recipientError = new TxIntentMismatchRecipientError(
154+
'Recipient error',
155+
mockTransactionId,
156+
mockTxParams,
157+
mockTxHex,
158+
[]
159+
);
160+
const contractError = new TxIntentMismatchContractError(
161+
'Contract error',
162+
mockTransactionId,
163+
mockTxParams,
164+
mockTxHex,
165+
{ address: '0xtest', rawContractPayload: '0x', decodedContractPayload: {} }
166+
);
167+
const approvalError = new TxIntentMismatchApprovalError(
168+
'Approval error',
169+
mockTransactionId,
170+
mockTxParams,
171+
mockTxHex,
172+
{ tokenAddress: '0xtoken', authorizingAmount: 1000, authorizingAddress: '0xspender' }
173+
);
174+
175+
// All should be instances of Error
176+
should(baseError).be.instanceOf(Error);
177+
should(recipientError).be.instanceOf(Error);
178+
should(contractError).be.instanceOf(Error);
179+
should(approvalError).be.instanceOf(Error);
180+
181+
// All should be instances of TxIntentMismatchError
182+
should(recipientError).be.instanceOf(TxIntentMismatchError);
183+
should(contractError).be.instanceOf(TxIntentMismatchError);
184+
should(approvalError).be.instanceOf(TxIntentMismatchError);
185+
});
186+
187+
it('should preserve stack trace', () => {
188+
const error = new TxIntentMismatchError('Test error', mockTransactionId, mockTxParams, mockTxHex);
189+
190+
should.exist(error.stack);
191+
should(error.stack).be.a.String();
192+
should(error.stack).containEql('TxIntentMismatchError');
193+
});
194+
});
195+
});

0 commit comments

Comments
 (0)