Skip to content

Commit 5ff911c

Browse files
committed
Add crosschain swap and checks
1 parent 5058681 commit 5ff911c

7 files changed

Lines changed: 57 additions & 25 deletions

File tree

packages/lib-ts/src/intents/Intent.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { SvmAccountMeta } from '../types/svm/SvmAccountMeta'
77

88
import { EvmCall, EvmCallData } from './Call/EvmCall'
99
import { SvmCall, SvmInstruction } from './Call/SvmCall'
10-
import { Operation, OperationBuilder, OperationEvent } from './Operation'
10+
import { Operation, OperationBuilder, OperationEvent, OperationType } from './Operation'
1111
import { Swap, SwapTokenIn, SwapTokenOut } from './Swap'
1212
import { Transfer, TransferData } from './Transfer'
1313

@@ -333,8 +333,18 @@ export class Intent {
333333
this.operations = operations || []
334334
if (this.operations.length === 0) throw new Error('Operation list cannot be empty')
335335

336-
this.maxFees = maxFees ? maxFees.map((fee: TokenAmount) => MaxFee.fromTokenAmount(fee)) : []
337336
const defaultChainId = this.operations[0].chainId
337+
for (let i = 0; i < this.operations.length; i++) {
338+
const operation = this.operations[i]
339+
340+
if (operation.chainId !== defaultChainId) throw new Error('All operations must have the same chainId')
341+
342+
if (operation.opType === OperationType.CrossChainSwap) {
343+
if (this.operations.length > 1) throw new Error('Cross-chain swap must be the only operation in an intent')
344+
}
345+
}
346+
347+
this.maxFees = maxFees ? maxFees.map((fee: TokenAmount) => MaxFee.fromTokenAmount(fee)) : []
338348
this.settler = settler ? settler.toString() : context.findSettler(defaultChainId).toString()
339349
this.feePayer = feePayer ? feePayer.toString() : context.user.toString()
340350
this.deadline = deadline ? deadline.toString() : (context.timestamp / 1000 + DEFAULT_DEADLINE).toString()

packages/lib-ts/src/intents/Operation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export enum OperationType {
66
Swap,
77
Transfer,
88
EvmCall,
9+
CrossChainSwap,
910
SvmCall,
1011
}
1112

packages/lib-ts/src/intents/Swap.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -392,20 +392,6 @@ export class SwapTokenOut {
392392
*/
393393
@json
394394
export class Swap extends Operation {
395-
/**
396-
* Creates a simple single-chain swap intent.
397-
* @param chainId - The blockchain network identifier
398-
* @param tokenIn - The input token
399-
* @param amountIn - The amount to swap from
400-
* @param tokenOut - The output token
401-
* @param minAmountOut - The minimum amount to receive
402-
* @param settler - The settler address (optional)
403-
* @param recipient - The recipient address (optional)
404-
* @param deadline - The deadline timestamp (optional)
405-
* @param nonce - The nonce for replay protection (optional)
406-
* @returns A new Swap instance
407-
*/
408-
409395
/**
410396
* Creates a new Swap intent.
411397
* @param sourceChain - The source blockchain network identifier
@@ -426,7 +412,8 @@ export class Swap extends Operation {
426412
user: Address | null = null,
427413
events: OperationEvent[] | null = null
428414
) {
429-
super(OperationType.Swap, sourceChain, user, events)
415+
const opType = sourceChain == destinationChain ? OperationType.Swap : OperationType.CrossChainSwap
416+
super(opType, sourceChain, user, events)
430417
if (tokensIn.length === 0) throw new Error('TokenIn list cannot be empty')
431418
if (tokensOut.length === 0) throw new Error('TokenOut list cannot be empty')
432419
}

packages/lib-ts/tests/intents/Intent.spec.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { JSON } from 'json-as'
22

33
import { SerializableSettler } from '../../src/context'
44
import { NULL_ADDRESS } from '../../src/helpers'
5-
import { EvmCallBuilder, IntentBuilder } from '../../src/intents'
5+
import { EvmCallBuilder, IntentBuilder, SwapBuilder } from '../../src/intents'
66
import { TokenAmount } from '../../src/tokens'
77
import { Address, BigInt, Bytes } from '../../src/types'
88
import { randomERC20Token, randomSettler, setContext } from '../helpers'
@@ -123,4 +123,38 @@ describe('IntentBuilder', () => {
123123
}).toThrow('Operation list cannot be empty')
124124
})
125125
})
126+
127+
describe('when operations have different chainIds', () => {
128+
it('throws an error', () => {
129+
const settler = randomSettler(chainId)
130+
setContext(0, 1, '0x0000000000000000000000000000000000000002', [settler], 'trigger-call')
131+
132+
expect(() => {
133+
new IntentBuilder()
134+
.addOperationBuilder(EvmCallBuilder.forChain(1).addCall(Address.fromString(targetAddressStr)))
135+
.addOperationBuilder(EvmCallBuilder.forChain(10).addCall(Address.fromString(targetAddressStr)))
136+
.build()
137+
}).toThrow('All operations must have the same chainId')
138+
})
139+
})
140+
141+
describe('when a cross-chain swap is combined with another operation', () => {
142+
it('throws an error', () => {
143+
const settler = randomSettler(chainId)
144+
setContext(0, 1, '0x0000000000000000000000000000000000000002', [settler], 'trigger-call')
145+
146+
expect(() => {
147+
const tokenIn = randomERC20Token(1)
148+
const tokenOut = randomERC20Token(10)
149+
new IntentBuilder()
150+
.addOperationBuilder(
151+
SwapBuilder.forChains(1, 10)
152+
.addTokenInFromStringDecimal(tokenIn, '1')
153+
.addTokenOutFromStringDecimal(tokenOut, '1', Address.fromString(targetAddressStr))
154+
)
155+
.addOperationBuilder(EvmCallBuilder.forChain(1).addCall(Address.fromString(targetAddressStr)))
156+
.build()
157+
}).toThrow('Cross-chain swap must be the only operation in an intent')
158+
})
159+
})
126160
})

packages/lib-ts/tests/intents/SvmCall.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('SvmCall', () => {
3636
expect(svmCall.events.length).toBe(0)
3737

3838
expect(JSON.stringify(svmCall)).toBe(
39-
`{"opType":3,"chainId":${ChainId.SOLANA_MAINNET},"user":"${user}","events":[],"instructions":[{"programId":"${programId.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta[0].pubkey}","isWritable":${accountsMeta[0].isWritable},"isSigner":${accountsMeta[0].isSigner}}],"data":"${data.toHexString()}"}]}`
39+
`{"opType":4,"chainId":${ChainId.SOLANA_MAINNET},"user":"${user}","events":[],"instructions":[{"programId":"${programId.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta[0].pubkey}","isWritable":${accountsMeta[0].isWritable},"isSigner":${accountsMeta[0].isSigner}}],"data":"${data.toHexString()}"}]}`
4040
)
4141
})
4242

@@ -69,7 +69,7 @@ describe('SvmCall', () => {
6969
expect(svmCall.events[0].data).toBe('0x64617461')
7070

7171
expect(JSON.stringify(svmCall)).toBe(
72-
`{"opType":3,"chainId":${ChainId.SOLANA_MAINNET},"user":"${user}","events":[{"topic":"0x746f706963","data":"0x64617461"}],"instructions":[{"programId":"${programId.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta[0].pubkey}","isWritable":${accountsMeta[0].isWritable},"isSigner":${accountsMeta[0].isSigner}}],"data":"${data.toHexString()}"}]}`
72+
`{"opType":4,"chainId":${ChainId.SOLANA_MAINNET},"user":"${user}","events":[{"topic":"0x746f706963","data":"0x64617461"}],"instructions":[{"programId":"${programId.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta[0].pubkey}","isWritable":${accountsMeta[0].isWritable},"isSigner":${accountsMeta[0].isSigner}}],"data":"${data.toHexString()}"}]}`
7373
)
7474
})
7575
})
@@ -107,7 +107,7 @@ describe('SvmCall', () => {
107107
expect(svmCall.events.length).toBe(0)
108108

109109
expect(JSON.stringify(svmCall)).toBe(
110-
`{"opType":3,"chainId":${ChainId.SOLANA_MAINNET},"user":"${user}","events":[],"instructions":[{"programId":"${programId1.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta1[0].pubkey}","isWritable":${accountsMeta1[0].isWritable},"isSigner":${accountsMeta1[0].isSigner}}],"data":"${data1.toHexString()}"},{"programId":"${programId2.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta2[0].pubkey}","isWritable":${accountsMeta2[0].isWritable},"isSigner":${accountsMeta2[0].isSigner}}],"data":"${data2.toHexString()}"}]}`
110+
`{"opType":4,"chainId":${ChainId.SOLANA_MAINNET},"user":"${user}","events":[],"instructions":[{"programId":"${programId1.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta1[0].pubkey}","isWritable":${accountsMeta1[0].isWritable},"isSigner":${accountsMeta1[0].isSigner}}],"data":"${data1.toHexString()}"},{"programId":"${programId2.toBase58String()}","accountsMeta":[{"pubkey":"${accountsMeta2[0].pubkey}","isWritable":${accountsMeta2[0].isWritable},"isSigner":${accountsMeta2[0].isSigner}}],"data":"${data2.toHexString()}"}]}`
111111
)
112112
})
113113
})

packages/lib-ts/tests/intents/Swap.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('Swap', () => {
5656
setContext(1, 1, user.toString(), [settler], 'trigger-456')
5757

5858
const swap = new Swap(chainId, [tokenIn], [tokenOut], destinationChain, randomUser, [])
59-
expect(swap.opType).toBe(OperationType.Swap)
59+
expect(swap.opType).toBe(OperationType.CrossChainSwap)
6060
expect(swap.user).toBe(randomUser.toString())
6161

6262
expect(swap.sourceChain).toBe(chainId)
@@ -73,7 +73,7 @@ describe('Swap', () => {
7373
expect(swap.events.length).toBe(0)
7474

7575
expect(JSON.stringify(swap)).toBe(
76-
`{"opType":0,"chainId":${chainId},"user":"${randomUser}","events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.token}","amount":"${tokenIn.amount}"}],"tokensOut":[{"token":"${tokenOut.token}","minAmount":"${tokenOut.minAmount}","recipient":"${tokenOut.recipient}"}],"destinationChain":${destinationChain}}`
76+
`{"opType":3,"chainId":${chainId},"user":"${randomUser}","events":[],"sourceChain":${chainId},"tokensIn":[{"token":"${tokenIn.token}","amount":"${tokenIn.amount}"}],"tokensOut":[{"token":"${tokenOut.token}","minAmount":"${tokenOut.minAmount}","recipient":"${tokenOut.recipient}"}],"destinationChain":${destinationChain}}`
7777
)
7878
})
7979

@@ -115,7 +115,7 @@ describe('SwapBuilder', () => {
115115
builder.addTokenOutFromTokenAmount(tokenOutAmount, recipientAddress)
116116

117117
const swap = builder.build()
118-
expect(swap.opType).toBe(OperationType.Swap)
118+
expect(swap.opType).toBe(OperationType.CrossChainSwap)
119119
expect(swap.sourceChain).toBe(sourceChain)
120120
expect(swap.destinationChain).toBe(destinationChain)
121121
expect(swap.tokensIn[0].token).toBe(tokenInAddress.toString())

packages/test-ts/src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function toIntents(intentsJson: string) {
3030
return {
3131
...intent,
3232
operations: intent.operations.map((operation: Operation) => {
33-
if (operation.opType == OpType.Swap) {
33+
if (operation.opType == OpType.Swap || operation.opType == OpType.CrossChainSwap) {
3434
const swap = operation as SwapOperation
3535
return {
3636
...swap,

0 commit comments

Comments
 (0)