Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions modules/sdk-core/src/bitgo/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Descriptive error types for common issues which may arise
// during the operation of BitGoJS or BitGoExpress
import { BitGoJsError } from '../bitgojsError';
import { IRequestTracer } from '../api/types';
import { TransactionParams } from './baseCoin';
import { SendManyOptions } from './wallet';

// re-export for backwards compat
export { BitGoJsError };
Expand Down Expand Up @@ -197,3 +200,182 @@ export class ApiResponseError<ResponseBodyType = any> extends BitGoJsError {
this.needsOTP = needsOTP;
}
}

/**
* Interface for token approval information used in suspicious transaction detection
*
* @interface TokenApproval
* @property {string} [tokenName] - Optional human-readable name of the token
* @property {string} tokenAddress - The contract address of the token being approved
* @property {Object} authorizingAmount - The amount being authorized for spending
* @property {string} authorizingAddress - The address being authorized to spend the tokens
*/
export interface TokenApproval {
tokenName?: string;
tokenAddress: string;
authorizingAmount: { type: 'unlimited' } | { type: 'limited'; amount: number };
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment on line 210 incorrectly states the type as number | 'unlimited' but the actual TypeScript type is a discriminated union with { type: 'unlimited' } or { type: 'limited'; amount: number }. The documentation should be updated to match the implementation.

Copilot uses AI. Check for mistakes.

authorizingAddress: string;
}

/**
* Interface for mismatched recipient information detected during transaction verification
*
* @interface MismatchedRecipient
* @property {string} address - The recipient address that was found in the transaction
* @property {string} amount - The amount being sent to this recipient
* @property {string | TokenTransferRecipientParams} [data] - Optional transaction data or token transfer parameters
* @property {string} [tokenName] - Optional name of the token being transferred
* @property {TokenTransferRecipientParams} [tokenData] - Optional structured token transfer data
*/
export type MismatchedRecipient = NonNullable<SendManyOptions['recipients']>[0];

/**
* Interface for contract interaction data payload used in suspicious transaction detection
*
* @interface ContractDataPayload
* @property {string} address - The contract address being interacted with
* @property {string} rawContractPayload - The raw contract payload in serialized form specific to the blockchain
* @property {unknown} decodedContractPayload - The decoded contract payload, structure varies by coin/chain implementation
*/
export interface ContractDataPayload {
address: string;
// The raw contract payload in serialized form of the chain
rawContractPayload: string;
// To be defined on a per-coin basis
decodedContractPayload: unknown;
}

/**
* Base error class for transaction intent mismatch detection
*
* This error is thrown when a transaction does not match the user's original intent,
* indicating potential security issues or malicious modifications.
*
* @class TxIntentMismatchError
* @extends {BitGoJsError}
* @property {string | IRequestTracer} id - Transaction ID or request tracer for tracking
* @property {TransactionParams[]} txParams - Array of transaction parameters that were analyzed
* @property {string} txHex - The raw transaction in hexadecimal format
*/
export class TxIntentMismatchError extends BitGoJsError {
public readonly id: string | IRequestTracer;
public readonly txParams: TransactionParams[];
public readonly txHex: string;

/**
* Creates an instance of TxIntentMismatchError
*
* @param {string} message - Error message describing the intent mismatch
* @param {string | IRequestTracer} id - Transaction ID or request tracer
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
* @param {string} txHex - Raw transaction hex string
*/
public constructor(message: string, id: string | IRequestTracer, txParams: TransactionParams[], txHex: string) {
super(message);
this.id = id;
this.txParams = txParams;
this.txHex = txHex;
}
}

/**
* Error thrown when transaction recipients don't match the user's intent
*
* This error occurs when the transaction contains recipients or amounts that differ
* from what the user originally intended to send.
*
* @class TxIntentMismatchRecipientError
* @extends {TxIntentMismatchError}
* @property {MismatchedRecipient[]} mismatchedRecipients - Array of recipients that don't match user intent
*/
export class TxIntentMismatchRecipientError extends TxIntentMismatchError {
public readonly mismatchedRecipients: MismatchedRecipient[];

/**
* Creates an instance of TxIntentMismatchRecipientError
*
* @param {string} message - Error message describing the recipient intent mismatch
* @param {string | IRequestTracer} id - Transaction ID or request tracer
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
* @param {string} txHex - Raw transaction hex string
* @param {MismatchedRecipient[]} mismatchedRecipients - Array of recipients that don't match user intent
*/
public constructor(
message: string,
id: string | IRequestTracer,
txParams: TransactionParams[],
txHex: string,
mismatchedRecipients: MismatchedRecipient[]
) {
super(message, id, txParams, txHex);
this.mismatchedRecipients = mismatchedRecipients;
}
}

/**
* Error thrown when contract interaction doesn't match the user's intent
*
* This error occurs when a transaction interacts with a smart contract but the
* contract call data or method doesn't match what the user intended.
*
* @class TxIntentMismatchContractError
* @extends {TxIntentMismatchError}
* @property {ContractDataPayload} mismatchedDataPayload - The contract interaction data that doesn't match user intent
*/
export class TxIntentMismatchContractError extends TxIntentMismatchError {
public readonly mismatchedDataPayload: ContractDataPayload;

/**
* Creates an instance of TxIntentMismatchContractError
*
* @param {string} message - Error message describing the contract intent mismatch
* @param {string | IRequestTracer} id - Transaction ID or request tracer
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
* @param {string} txHex - Raw transaction hex string
* @param {ContractDataPayload} mismatchedDataPayload - The contract interaction data that doesn't match user intent
*/
public constructor(
message: string,
id: string | IRequestTracer,
txParams: TransactionParams[],
txHex: string,
mismatchedDataPayload: ContractDataPayload
) {
super(message, id, txParams, txHex);
this.mismatchedDataPayload = mismatchedDataPayload;
}
}

/**
* Error thrown when token approval doesn't match the user's intent
*
* This error occurs when a transaction contains a token approval that the user
* did not intend to authorize, potentially indicating malicious activity.
*
* @class TxIntentMismatchApprovalError
* @extends {TxIntentMismatchError}
* @property {TokenApproval} tokenApproval - Details of the token approval that doesn't match user intent
*/
export class TxIntentMismatchApprovalError extends TxIntentMismatchError {
public readonly tokenApproval: TokenApproval;

/**
* Creates an instance of TxIntentMismatchApprovalError
*
* @param {string} message - Error message describing the approval intent mismatch
* @param {string | IRequestTracer} id - Transaction ID or request tracer
* @param {TransactionParams[]} txParams - Transaction parameters that were analyzed
* @param {string} txHex - Raw transaction hex string
* @param {TokenApproval} tokenApproval - Details of the token approval that doesn't match user intent
*/
public constructor(
message: string,
id: string | IRequestTracer,
txParams: TransactionParams[],
txHex: string,
tokenApproval: TokenApproval
) {
super(message, id, txParams, txHex);
this.tokenApproval = tokenApproval;
}
}
199 changes: 199 additions & 0 deletions modules/sdk-core/test/unit/bitgo/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import should from 'should';
import {
TxIntentMismatchError,
TxIntentMismatchRecipientError,
TxIntentMismatchContractError,
TxIntentMismatchApprovalError,
MismatchedRecipient,
ContractDataPayload,
TokenApproval,
} from '../../../src/bitgo/errors';

describe('Transaction Intent Mismatch Errors', () => {
const mockTransactionId = '0x1234567890abcdef';
const mockTxParams: any[] = [
{ address: '0xrecipient1', amount: '1000000000000000000' },
{ address: '0xrecipient2', amount: '2000000000000000000' },
];
const mockTxHex = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';

describe('TxIntentMismatchError', () => {
it('should create base transaction intent mismatch error with all required properties', () => {
const message = 'Transaction does not match user intent';
const error = new TxIntentMismatchError(message, mockTransactionId, mockTxParams, mockTxHex);

should.exist(error);
should.equal(error.message, message);
should.equal(error.name, 'TxIntentMismatchError');
should.equal(error.id, mockTransactionId);
should.deepEqual(error.txParams, mockTxParams);
should.equal(error.txHex, mockTxHex);
});

it('should be an instance of Error', () => {
const error = new TxIntentMismatchError('Test message', mockTransactionId, mockTxParams, mockTxHex);

should(error).be.instanceOf(Error);
});
});

describe('TxIntentMismatchRecipientError', () => {
it('should create recipient intent mismatch error with mismatched recipients', () => {
const message = 'Transaction recipients do not match user intent';
const mismatchedRecipients: MismatchedRecipient[] = [
{ address: '0xexpected1', amount: '1000' },
{ address: '0xexpected2', amount: '2000' },
];

const error = new TxIntentMismatchRecipientError(
message,
mockTransactionId,
mockTxParams,
mockTxHex,
mismatchedRecipients
);

should.exist(error);
should.equal(error.message, message);
should.equal(error.name, 'TxIntentMismatchRecipientError');
should.equal(error.id, mockTransactionId);
should.deepEqual(error.txParams, mockTxParams);
should.equal(error.txHex, mockTxHex);
should.deepEqual(error.mismatchedRecipients, mismatchedRecipients);
});

it('should be an instance of TxIntentMismatchError', () => {
const error = new TxIntentMismatchRecipientError('Test message', mockTransactionId, mockTxParams, mockTxHex, []);

should(error).be.instanceOf(TxIntentMismatchError);
should(error).be.instanceOf(Error);
});
});

describe('TxIntentMismatchContractError', () => {
it('should create contract intent mismatch error with mismatched data payload', () => {
const message = 'Contract interaction does not match user intent';
const mismatchedDataPayload: ContractDataPayload = {
address: '0xcontract123',
rawContractPayload: '0xabcdef',
decodedContractPayload: { method: 'transfer', params: ['0xrecipient', '1000'] },
};

const error = new TxIntentMismatchContractError(
message,
mockTransactionId,
mockTxParams,
mockTxHex,
mismatchedDataPayload
);

should.exist(error);
should.equal(error.message, message);
should.equal(error.name, 'TxIntentMismatchContractError');
should.equal(error.id, mockTransactionId);
should.deepEqual(error.txParams, mockTxParams);
should.equal(error.txHex, mockTxHex);
should.deepEqual(error.mismatchedDataPayload, mismatchedDataPayload);
});

it('should be an instance of TxIntentMismatchError', () => {
const error = new TxIntentMismatchContractError('Test message', mockTransactionId, mockTxParams, mockTxHex, {
address: '0xtest',
rawContractPayload: '0x',
decodedContractPayload: {},
});

should(error).be.instanceOf(TxIntentMismatchError);
should(error).be.instanceOf(Error);
});
});

describe('TxIntentMismatchApprovalError', () => {
it('should create approval intent mismatch error with token approval details', () => {
const message = 'Token approval does not match user intent';
const tokenApproval: TokenApproval = {
tokenName: 'TestToken',
tokenAddress: '0xtoken123',
authorizingAmount: { type: 'unlimited' },
authorizingAddress: '0xspender456',
};

const error = new TxIntentMismatchApprovalError(
message,
mockTransactionId,
mockTxParams,
mockTxHex,
tokenApproval
);

should.exist(error);
should.equal(error.message, message);
should.equal(error.name, 'TxIntentMismatchApprovalError');
should.equal(error.id, mockTransactionId);
should.deepEqual(error.txParams, mockTxParams);
should.equal(error.txHex, mockTxHex);
should.deepEqual(error.tokenApproval, tokenApproval);
});

it('should be an instance of TxIntentMismatchError', () => {
const error = new TxIntentMismatchApprovalError('Test message', mockTransactionId, mockTxParams, mockTxHex, {
tokenAddress: '0xtoken',
authorizingAmount: { type: 'limited', amount: 1000 },
authorizingAddress: '0xspender',
});

should(error).be.instanceOf(TxIntentMismatchError);
should(error).be.instanceOf(Error);
});
});

describe('Error inheritance and properties', () => {
it('should maintain proper inheritance chain', () => {
const baseError = new TxIntentMismatchError('Base error', mockTransactionId, mockTxParams, mockTxHex);
const recipientError = new TxIntentMismatchRecipientError(
'Recipient error',
mockTransactionId,
mockTxParams,
mockTxHex,
[]
);
const contractError = new TxIntentMismatchContractError(
'Contract error',
mockTransactionId,
mockTxParams,
mockTxHex,
{ address: '0xtest', rawContractPayload: '0x', decodedContractPayload: {} }
);
const approvalError = new TxIntentMismatchApprovalError(
'Approval error',
mockTransactionId,
mockTxParams,
mockTxHex,
{
tokenAddress: '0xtoken',
authorizingAmount: { type: 'limited', amount: 1000 },
authorizingAddress: '0xspender',
}
);

// All should be instances of Error
should(baseError).be.instanceOf(Error);
should(recipientError).be.instanceOf(Error);
should(contractError).be.instanceOf(Error);
should(approvalError).be.instanceOf(Error);

// All should be instances of TxIntentMismatchError
should(recipientError).be.instanceOf(TxIntentMismatchError);
should(contractError).be.instanceOf(TxIntentMismatchError);
should(approvalError).be.instanceOf(TxIntentMismatchError);
});

it('should preserve stack trace', () => {
const error = new TxIntentMismatchError('Test error', mockTransactionId, mockTxParams, mockTxHex);

should.exist(error.stack);
should(error.stack).be.a.String();
should(error.stack).containEql('TxIntentMismatchError');
});
});
});