Skip to content

Commit 7bb4402

Browse files
authored
Merge pull request #13 from openSVM/copilot/fix-12
Enable cross-chain payments: pay in EVM network via bridges, pay Solana using bridged EVM assets
2 parents e78b511 + deed33a commit 7bb4402

File tree

370 files changed

+29200
-31326
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

370 files changed

+29200
-31326
lines changed
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
/**
2+
* Tests for cross-chain payment functionality
3+
*/
4+
5+
import {
6+
CrossChainTransferRequest,
7+
RequestType,
8+
SVMNetwork,
9+
EVMNetwork,
10+
BridgeQuote,
11+
BridgeTransferStatus,
12+
PaymentStatus
13+
} from '../../src/core/types';
14+
import { CrossChainPaymentManager, CrossChainRequestFactory } from '../../src/core/cross-chain';
15+
import { WormholeBridgeAdapter } from '../../src/bridge/wormhole';
16+
import { AllbridgeBridgeAdapter } from '../../src/bridge/allbridge';
17+
import { BridgeAdapterFactory } from '../../src/bridge/adapter';
18+
import { getBestBridgeQuote, validateCrossChainRequest } from '../../src/bridge/utils';
19+
20+
describe('Cross-Chain Payment Functionality', () => {
21+
let wormholeAdapter: WormholeBridgeAdapter;
22+
let allbridgeAdapter: AllbridgeBridgeAdapter;
23+
let paymentManager: CrossChainPaymentManager;
24+
25+
beforeEach(() => {
26+
// Clear any existing adapters
27+
BridgeAdapterFactory['adapters'].clear();
28+
29+
// Create bridge adapters
30+
wormholeAdapter = new WormholeBridgeAdapter();
31+
allbridgeAdapter = new AllbridgeBridgeAdapter();
32+
33+
// Register adapters
34+
BridgeAdapterFactory.registerAdapter(wormholeAdapter);
35+
BridgeAdapterFactory.registerAdapter(allbridgeAdapter);
36+
37+
// Create payment manager
38+
paymentManager = new CrossChainPaymentManager();
39+
});
40+
41+
describe('CrossChainRequestFactory', () => {
42+
it('should create a valid cross-chain transfer request', () => {
43+
const request = CrossChainRequestFactory.createTransferRequest({
44+
sourceNetwork: EVMNetwork.ETHEREUM,
45+
destinationNetwork: SVMNetwork.SOLANA,
46+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
47+
amount: '100',
48+
token: '0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F', // USDC on Ethereum
49+
bridge: 'wormhole',
50+
label: 'Test Payment',
51+
message: 'Cross-chain test',
52+
memo: 'test-memo'
53+
});
54+
55+
expect(request.type).toBe(RequestType.CROSS_CHAIN_TRANSFER);
56+
expect(request.sourceNetwork).toBe(EVMNetwork.ETHEREUM);
57+
expect(request.destinationNetwork).toBe(SVMNetwork.SOLANA);
58+
expect(request.amount).toBe('100');
59+
expect(request.token).toBe('0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F');
60+
expect(request.bridge).toBe('wormhole');
61+
});
62+
});
63+
64+
describe('Bridge Adapters', () => {
65+
describe('WormholeBridgeAdapter', () => {
66+
it('should support transfer from Ethereum to Solana for USDC', () => {
67+
const supported = wormholeAdapter.supportsTransfer(
68+
EVMNetwork.ETHEREUM,
69+
SVMNetwork.SOLANA,
70+
'0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F' // USDC
71+
);
72+
expect(supported).toBe(true);
73+
});
74+
75+
it('should not support unsupported token', () => {
76+
const supported = wormholeAdapter.supportsTransfer(
77+
EVMNetwork.ETHEREUM,
78+
SVMNetwork.SOLANA,
79+
'0x1234567890123456789012345678901234567890' // Random token
80+
);
81+
expect(supported).toBe(false);
82+
});
83+
84+
it('should generate a valid quote', async () => {
85+
const request: CrossChainTransferRequest = {
86+
type: RequestType.CROSS_CHAIN_TRANSFER,
87+
network: SVMNetwork.SOLANA,
88+
sourceNetwork: EVMNetwork.ETHEREUM,
89+
destinationNetwork: SVMNetwork.SOLANA,
90+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
91+
amount: '100',
92+
token: '0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F'
93+
};
94+
95+
const quote = await wormholeAdapter.quote(request);
96+
97+
expect(quote.id).toContain('wormhole-');
98+
expect(quote.inputAmount).toBe('100');
99+
expect(parseFloat(quote.outputAmount)).toBeLessThan(100); // Due to fees
100+
expect(parseFloat(quote.fee)).toBeGreaterThan(0);
101+
expect(quote.estimatedTime).toBe(300); // 5 minutes
102+
});
103+
});
104+
105+
describe('AllbridgeBridgeAdapter', () => {
106+
it('should support transfer from Ethereum to Solana for USDC', () => {
107+
const supported = allbridgeAdapter.supportsTransfer(
108+
EVMNetwork.ETHEREUM,
109+
SVMNetwork.SOLANA,
110+
'0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F' // USDC
111+
);
112+
expect(supported).toBe(true);
113+
});
114+
115+
it('should generate a valid quote with lower fees than Wormhole', async () => {
116+
const request: CrossChainTransferRequest = {
117+
type: RequestType.CROSS_CHAIN_TRANSFER,
118+
network: SVMNetwork.SOLANA,
119+
sourceNetwork: EVMNetwork.ETHEREUM,
120+
destinationNetwork: SVMNetwork.SOLANA,
121+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
122+
amount: '100',
123+
token: '0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F'
124+
};
125+
126+
const [wormholeQuote, allbridgeQuote] = await Promise.all([
127+
wormholeAdapter.quote(request),
128+
allbridgeAdapter.quote(request)
129+
]);
130+
131+
// Allbridge should have lower fees
132+
expect(parseFloat(allbridgeQuote.fee)).toBeLessThan(parseFloat(wormholeQuote.fee));
133+
expect(allbridgeQuote.estimatedTime).toBeLessThan(wormholeQuote.estimatedTime);
134+
});
135+
});
136+
});
137+
138+
describe('BridgeAdapterFactory', () => {
139+
it('should find compatible adapters for a transfer', () => {
140+
const compatibleAdapters = BridgeAdapterFactory.findCompatibleAdapters(
141+
EVMNetwork.ETHEREUM,
142+
SVMNetwork.SOLANA,
143+
'0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F' // USDC
144+
);
145+
146+
expect(compatibleAdapters).toHaveLength(2); // Wormhole and Allbridge
147+
expect(compatibleAdapters.map(a => a.info.id)).toContain('wormhole');
148+
expect(compatibleAdapters.map(a => a.info.id)).toContain('allbridge');
149+
});
150+
151+
it('should return empty array for unsupported transfers', () => {
152+
const compatibleAdapters = BridgeAdapterFactory.findCompatibleAdapters(
153+
EVMNetwork.ETHEREUM,
154+
SVMNetwork.SOLANA,
155+
'0x1234567890123456789012345678901234567890' // Unsupported token
156+
);
157+
158+
expect(compatibleAdapters).toHaveLength(0);
159+
});
160+
});
161+
162+
describe('Bridge Utils', () => {
163+
it('should validate a valid cross-chain request', () => {
164+
const request: CrossChainTransferRequest = {
165+
type: RequestType.CROSS_CHAIN_TRANSFER,
166+
network: SVMNetwork.SOLANA,
167+
sourceNetwork: EVMNetwork.ETHEREUM,
168+
destinationNetwork: SVMNetwork.SOLANA,
169+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
170+
amount: '100',
171+
token: '0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F'
172+
};
173+
174+
expect(() => validateCrossChainRequest(request)).not.toThrow();
175+
});
176+
177+
it('should throw for invalid cross-chain request', () => {
178+
const request: CrossChainTransferRequest = {
179+
type: RequestType.CROSS_CHAIN_TRANSFER,
180+
network: SVMNetwork.SOLANA,
181+
sourceNetwork: SVMNetwork.SOLANA, // Same as destination
182+
destinationNetwork: SVMNetwork.SOLANA,
183+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
184+
amount: '100',
185+
token: '0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F'
186+
};
187+
188+
expect(() => validateCrossChainRequest(request)).toThrow('Source and destination networks cannot be the same');
189+
});
190+
191+
it('should get the best bridge quote', async () => {
192+
const request: CrossChainTransferRequest = {
193+
type: RequestType.CROSS_CHAIN_TRANSFER,
194+
network: SVMNetwork.SOLANA,
195+
sourceNetwork: EVMNetwork.ETHEREUM,
196+
destinationNetwork: SVMNetwork.SOLANA,
197+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
198+
amount: '100',
199+
token: '0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F'
200+
};
201+
202+
const result = await getBestBridgeQuote(request);
203+
204+
expect(result).not.toBeNull();
205+
expect(result?.quote).toBeDefined();
206+
expect(result?.adapter).toBeDefined();
207+
208+
// Should select Allbridge due to lower fees and faster time
209+
expect(result?.adapter.info.id).toBe('allbridge');
210+
});
211+
});
212+
213+
describe('CrossChainPaymentManager', () => {
214+
it('should execute a cross-chain payment', async () => {
215+
const request: CrossChainTransferRequest = {
216+
type: RequestType.CROSS_CHAIN_TRANSFER,
217+
network: SVMNetwork.SOLANA,
218+
sourceNetwork: EVMNetwork.ETHEREUM,
219+
destinationNetwork: SVMNetwork.SOLANA,
220+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
221+
amount: '100',
222+
token: '0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F'
223+
};
224+
225+
const result = await paymentManager.executePayment(request);
226+
227+
expect(result.paymentId).toContain('cc-payment-');
228+
expect(result.bridge.info.id).toBe('allbridge'); // Should select best bridge
229+
expect(result.status).toBe(PaymentStatus.BRIDGING);
230+
expect(result.bridgeResult.transferId).toMatch(/allbridge-(transfer|fallback)-/); // Handle both real and fallback transfers
231+
});
232+
233+
it('should get payment status', async () => {
234+
const request: CrossChainTransferRequest = {
235+
type: RequestType.CROSS_CHAIN_TRANSFER,
236+
network: SVMNetwork.SOLANA,
237+
sourceNetwork: EVMNetwork.ETHEREUM,
238+
destinationNetwork: SVMNetwork.SOLANA,
239+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
240+
amount: '100',
241+
token: '0xa0B86a33E6441c4D0c85C81a1a4E18A3f3f3F77F'
242+
};
243+
244+
const result = await paymentManager.executePayment(request);
245+
const status = await paymentManager.getPaymentStatus(result.paymentId);
246+
247+
expect(status).not.toBeNull();
248+
expect(status?.id).toBe(result.paymentId);
249+
expect(status?.status).toBe(PaymentStatus.BRIDGING);
250+
expect(status?.bridgeUsed).toBe('allbridge');
251+
});
252+
});
253+
254+
describe('Negative Test Cases', () => {
255+
describe('Unsupported Token Quotes', () => {
256+
it('should throw error for unsupported token in Wormhole', async () => {
257+
const request: CrossChainTransferRequest = {
258+
type: RequestType.CROSS_CHAIN_TRANSFER,
259+
network: SVMNetwork.SOLANA,
260+
sourceNetwork: EVMNetwork.ETHEREUM,
261+
destinationNetwork: SVMNetwork.SOLANA,
262+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
263+
amount: '100',
264+
token: '0x1234567890123456789012345678901234567890' // Unsupported token
265+
};
266+
267+
await expect(wormholeAdapter.quote(request)).rejects.toThrow(
268+
'Wormhole does not support transfer from ethereum to solana for token 0x1234567890123456789012345678901234567890'
269+
);
270+
});
271+
272+
it('should throw error for unsupported token in Allbridge', async () => {
273+
const request: CrossChainTransferRequest = {
274+
type: RequestType.CROSS_CHAIN_TRANSFER,
275+
network: SVMNetwork.SOLANA,
276+
sourceNetwork: EVMNetwork.ETHEREUM,
277+
destinationNetwork: SVMNetwork.SOLANA,
278+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
279+
amount: '100',
280+
token: '0x9876543210987654321098765432109876543210' // Unsupported token
281+
};
282+
283+
await expect(allbridgeAdapter.quote(request)).rejects.toThrow(
284+
'Allbridge does not support transfer from ethereum to solana for token 0x9876543210987654321098765432109876543210'
285+
);
286+
});
287+
288+
it('should return null from getBestBridgeQuote for unsupported token', async () => {
289+
const request: CrossChainTransferRequest = {
290+
type: RequestType.CROSS_CHAIN_TRANSFER,
291+
network: SVMNetwork.SOLANA,
292+
sourceNetwork: EVMNetwork.ETHEREUM,
293+
destinationNetwork: SVMNetwork.SOLANA,
294+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
295+
amount: '100',
296+
token: '0x1111111111111111111111111111111111111111' // Unsupported token
297+
};
298+
299+
const result = await getBestBridgeQuote(request);
300+
expect(result).toBeNull();
301+
});
302+
303+
it('should throw error when no compatible bridges found', async () => {
304+
const request: CrossChainTransferRequest = {
305+
type: RequestType.CROSS_CHAIN_TRANSFER,
306+
network: SVMNetwork.SOLANA,
307+
sourceNetwork: EVMNetwork.ETHEREUM,
308+
destinationNetwork: SVMNetwork.SOLANA,
309+
recipient: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
310+
amount: '100',
311+
token: '0x2222222222222222222222222222222222222222' // Unsupported token
312+
};
313+
314+
await expect(paymentManager.executePayment(request)).rejects.toThrow(
315+
'No compatible bridges found for this transfer'
316+
);
317+
});
318+
});
319+
});
320+
});

0 commit comments

Comments
 (0)