Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The returned `NexusClient` includes:
- `swapWithExactOut(params, options?)`
- `swapAndExecute(params, options?)`
- `calculateMaxForSwap(params)`
- `calculateMaxForBridge(params)`
- Simulation variants: `simulateBridge`, `simulateBridgeAndTransfer`, `simulateBridgeAndExecute`, `simulateExecute`
- `getBalancesForBridge()`
- `getBalancesForSwap()`
Expand Down
9 changes: 9 additions & 0 deletions src/analytics/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,14 @@ export const NexusAnalyticsEvents = {
CALCULATE_MAX_FOR_SWAP_SUCCESS: 'nexus_v2_calculate_max_for_swap_success',
/** Fires when calculateMaxForSwap() throws. */
CALCULATE_MAX_FOR_SWAP_FAILED: 'nexus_v2_calculate_max_for_swap_failed',

// Calculate Max For Bridge Operations
/** Fires when calculateMaxForBridge() is called. */
CALCULATE_MAX_FOR_BRIDGE_INITIATED: 'nexus_v2_calculate_max_for_bridge_initiated',
/** Fires when calculateMaxForBridge() returns. */
CALCULATE_MAX_FOR_BRIDGE_SUCCESS: 'nexus_v2_calculate_max_for_bridge_success',
/** Fires when calculateMaxForBridge() throws. */
CALCULATE_MAX_FOR_BRIDGE_FAILED: 'nexus_v2_calculate_max_for_bridge_failed',
} as const;

export type NexusAnalyticsEvent = (typeof NexusAnalyticsEvents)[keyof typeof NexusAnalyticsEvents];
Expand All @@ -340,6 +348,7 @@ export const NexusOperationNames = {
BALANCES_FETCH_SWAP: 'balances_fetch_swap',
LIST_INTENTS: 'list_intents',
CALCULATE_MAX_FOR_SWAP: 'calculate_max_for_swap',
CALCULATE_MAX_FOR_BRIDGE: 'calculate_max_for_bridge',
WALLET_CONNECT: 'wallet_connect',
INITIALIZE: 'initialize',
} as const;
Expand Down
7 changes: 6 additions & 1 deletion src/core/sdk/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AnalyticsManager } from '../../analytics/AnalyticsManager';
import type { BridgeMaxParams, BridgeMaxResult } from '../../bridge/types';
import type {
AnalyticsConfig,
BridgeAndExecuteParams,
Expand Down Expand Up @@ -49,6 +50,7 @@ import {
trackBridgeAndExecute,
trackBridgeAndExecuteSim,
trackBridgeSim,
trackCalculateMaxForBridge,
trackCalculateMaxForSwap,
trackExecute,
trackExecuteSim,
Expand Down Expand Up @@ -192,6 +194,9 @@ export const createNexusClient = (config?: {
const calculateMaxForSwapPublic = (input: SwapMaxParams): Promise<SwapMaxResult> =>
trackCalculateMaxForSwap(analytics, input, () => base.calculateMaxForSwap(input));

const calculateMaxForBridgePublic = (input: BridgeMaxParams): Promise<BridgeMaxResult> =>
trackCalculateMaxForBridge(analytics, input, () => base.calculateMaxForBridge(input));

const setEVMProvider = (provider: EthereumProvider) => base.setEvmProvider(provider);

const convertTokenReadableAmountToBigInt = (
Expand Down Expand Up @@ -229,7 +234,7 @@ export const createNexusClient = (config?: {
swapWithExactOut,
swapAndExecute: swapAndExecutePublic,
calculateMaxForSwap: calculateMaxForSwapPublic,
calculateMaxForBridge: (input) => base.calculateMaxForBridge(input),
calculateMaxForBridge: calculateMaxForBridgePublic,
setEVMProvider,
convertTokenReadableAmountToBigInt,
getSupportedChains: () => getSupportedChainsFromChainList(base.getChainList()),
Expand Down
25 changes: 25 additions & 0 deletions src/core/sdk/operation-boundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
translateTransferEvent,
} from '../../analytics/lifecycle-translator';
import { buildEconomics, extractBridgeProperties, getWalletType } from '../../analytics/utils';
import type { BridgeMaxParams, BridgeMaxResult } from '../../bridge/types';
import type {
BridgeAndExecuteEvent,
BridgeAndExecuteParams,
Expand Down Expand Up @@ -617,6 +618,30 @@ export function trackCalculateMaxForSwap<R extends SwapMaxResult>(
});
}

export function trackCalculateMaxForBridge<R extends BridgeMaxResult>(
analytics: AnalyticsManager,
params: BridgeMaxParams,
run: (opId: string) => Promise<R>
): Promise<R> {
const initiatedProps = {
toChainId: params.toChainId,
tokenSymbol: params.toTokenSymbol,
sourceChains: params.sources,
};
return analytics.runOp<R>({
events: {
initiated: NexusAnalyticsEvents.CALCULATE_MAX_FOR_BRIDGE_INITIATED,
success: NexusAnalyticsEvents.CALCULATE_MAX_FOR_BRIDGE_SUCCESS,
failed: NexusAnalyticsEvents.CALCULATE_MAX_FOR_BRIDGE_FAILED,
},
opName: NexusOperationNames.CALCULATE_MAX_FOR_BRIDGE,
operation: 'calculateMaxForBridge',
initiatedProps,
params,
run,
});
}

export function trackWalletConnect<R extends { address: Hex; chainId: number }>(
analytics: AnalyticsManager,
provider: EthereumProvider,
Expand Down
3 changes: 2 additions & 1 deletion src/domain/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseError as ViemBaseError, type Hex } from 'viem';
import { type Hex, BaseError as ViemBaseError } from 'viem';

/**
* Categories for hierarchical errors. Drives subclass identity and `error.category`
Expand Down Expand Up @@ -42,6 +42,7 @@ export type OperationName =
| 'swapWithExactOut'
| 'swapAndExecute'
| 'calculateMaxForSwap'
| 'calculateMaxForBridge'
| 'setEVMProvider'
// exported utility helpers (rev 10)
| 'getCoinbaseRates'
Expand Down
124 changes: 124 additions & 0 deletions tests/core/sdk-calculate-max-for-bridge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { BridgeMaxParams, BridgeMaxResult } from '../../src';
import { createNexusClient } from '../../src';

const hoisted = vi.hoisted(() => ({
calculateMaxForBridge: vi.fn(),
peekChainList: vi.fn(),
reportOperationError: vi.fn(),
setAnalytics: vi.fn(),
}));

vi.mock('../../src/core/sdk/base', () => ({
createBase: vi.fn(() => ({
calculateMaxForBridge: hoisted.calculateMaxForBridge,
peekChainList: hoisted.peekChainList,
setAnalytics: hoisted.setAnalytics,
})),
}));

vi.mock('../../src/services/error-telemetry', () => ({
reportOperationError: hoisted.reportOperationError,
}));

const input: BridgeMaxParams = {
toChainId: 8453,
toTokenSymbol: 'USDC',
sources: [10, 42161],
};

const maxResult: BridgeMaxResult = {
toChainId: 8453,
toTokenSymbol: 'USDC',
provider: 'nexus',
maxAmount: '2.5',
maxAmountRaw: 2_500_000n,
symbol: 'USDC',
decimals: 6,
sources: [
{
chainId: 10,
tokenAddress: '0x0000000000000000000000000000000000000010',
symbol: 'USDC',
decimals: 6,
amount: '2.5',
},
],
};

const makeClient = () => {
const client = createNexusClient({
network: 'testnet',
analytics: { enabled: true },
});
client.analytics.enable();
return client;
};

describe('createNexusClient calculateMaxForBridge analytics', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('emits the operation lifecycle and preserves the max result', async () => {
hoisted.calculateMaxForBridge.mockResolvedValue(maxResult);
const client = makeClient();
const trackSpy = vi.spyOn(client.analytics, 'track');
trackSpy.mockClear();

await expect(client.calculateMaxForBridge(input)).resolves.toBe(maxResult);

expect(hoisted.calculateMaxForBridge).toHaveBeenCalledWith(input);
expect(trackSpy).toHaveBeenCalledWith('nexus_v2_calculate_max_for_bridge_initiated', {
toChainId: 8453,
tokenSymbol: 'USDC',
sourceChains: [10, 42161],
});
expect(trackSpy).toHaveBeenCalledWith('nexus_v2_calculate_max_for_bridge_success', {
toChainId: 8453,
tokenSymbol: 'USDC',
sourceChains: [10, 42161],
});
expect(trackSpy).toHaveBeenCalledWith(
'nexus_v2_operation_performance',
expect.objectContaining({
operation: 'calculate_max_for_bridge',
success: true,
})
);
});

it('emits failure analytics and rethrows the original error', async () => {
const failure = new Error('bridge max unavailable');
hoisted.calculateMaxForBridge.mockRejectedValue(failure);
const client = makeClient();
const trackSpy = vi.spyOn(client.analytics, 'track');
trackSpy.mockClear();

await expect(client.calculateMaxForBridge(input)).rejects.toBe(failure);

expect(trackSpy).toHaveBeenCalledWith('nexus_v2_calculate_max_for_bridge_failed', {
toChainId: 8453,
tokenSymbol: 'USDC',
sourceChains: [10, 42161],
});
expect(trackSpy).not.toHaveBeenCalledWith(
'nexus_v2_calculate_max_for_bridge_success',
expect.anything()
);
expect(hoisted.reportOperationError).toHaveBeenCalledWith({
operation: 'calculateMaxForBridge',
operationId: expect.any(String),
params: input,
options: undefined,
error: failure,
});
expect(trackSpy).toHaveBeenCalledWith(
'nexus_v2_operation_performance',
expect.objectContaining({
operation: 'calculate_max_for_bridge',
success: false,
})
);
});
});
14 changes: 14 additions & 0 deletions tests/public-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
IntentRecord,
ListIntentsParams,
ListIntentsResult,
OperationName,
SwapAndExecuteResult,
SwapMaxResult,
SwapResult as SwapResultType,
Expand All @@ -23,6 +24,8 @@ describe('public api exports', () => {
const bridgeSimulation = {} as BridgeSimulationResult;
const swapResult = {} as SwapResult;
const swapMaxResult = {} as SwapMaxResult;
const calculateMaxForBridgeOperation =
'calculateMaxForBridge' as const satisfies OperationName;
const txResult = {} as TxResult;
const bridgeAndExecuteResult = {} as BridgeAndExecuteResult;
const swapAndExecuteResult = {} as SwapAndExecuteResult;
Expand All @@ -33,6 +36,7 @@ describe('public api exports', () => {
expectTypeOf(bridgeSimulation).toMatchTypeOf<BridgeSimulationResult>();
expectTypeOf(swapResult).toMatchTypeOf<SwapResult>();
expectTypeOf(swapMaxResult).toMatchTypeOf<SwapMaxResult>();
expect(calculateMaxForBridgeOperation).toBe('calculateMaxForBridge');
expectTypeOf(txResult).toMatchTypeOf<TxResult>();
expectTypeOf(bridgeAndExecuteResult).toMatchTypeOf<BridgeAndExecuteResult>();
expectTypeOf(swapAndExecuteResult).toMatchTypeOf<SwapAndExecuteResult>();
Expand Down Expand Up @@ -83,6 +87,15 @@ describe('public api exports', () => {
expect(NexusAnalyticsEvents.CALCULATE_MAX_FOR_SWAP_FAILED).toBe(
'nexus_v2_calculate_max_for_swap_failed'
);
expect(NexusAnalyticsEvents.CALCULATE_MAX_FOR_BRIDGE_INITIATED).toBe(
'nexus_v2_calculate_max_for_bridge_initiated'
);
expect(NexusAnalyticsEvents.CALCULATE_MAX_FOR_BRIDGE_SUCCESS).toBe(
'nexus_v2_calculate_max_for_bridge_success'
);
expect(NexusAnalyticsEvents.CALCULATE_MAX_FOR_BRIDGE_FAILED).toBe(
'nexus_v2_calculate_max_for_bridge_failed'
);
});

it('locks the AnalyticsManager public surface after boundary cleanup', () => {
Expand Down Expand Up @@ -119,6 +132,7 @@ describe('public api exports', () => {
'trackBalanceFetch',
'trackInit',
'trackListIntents',
'trackCalculateMaxForBridge',
'trackCalculateMaxForSwap',
'trackWalletConnect',
]) {
Expand Down