Skip to content
This repository was archived by the owner on Jan 14, 2026. It is now read-only.

Commit 689b538

Browse files
committed
feat: add runtime validation for SwapComposer required parameters
Add validation in `SwapComposer` constructor to handle edge cases for JavaScript users who don't have TypeScript type checking. This prevents cryptic runtime errors by validating all required parameters upfront. Validates: - `quote`: must be provided (not null/undefined) - `deflexTxns`: must be provided and non-empty array - `algorand`: `AlgorandClient` instance must be provided - `address`: validated via existing `validateAddress` method Tests updated to: - Add comprehensive validation test cases for all required parameters - Fix existing tests that used empty `deflexTxns` arrays - Add `createMockDeflexTxn` helper to reduce test boilerplate - Update length assertions where `deflexTxns` adds transactions This ensures clear, actionable error messages for invalid configurations rather than allowing undefined behavior or obscure errors later in execution.
1 parent 035dcbc commit 689b538

File tree

3 files changed

+170
-37
lines changed

3 files changed

+170
-37
lines changed

src/composer.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ export class SwapComposer {
111111
* @param config.address - The address of the account that will sign transactions
112112
*/
113113
constructor(config: SwapComposerConfig) {
114+
// Validate required parameters
115+
if (!config.quote) {
116+
throw new Error('Quote is required')
117+
}
118+
if (!config.deflexTxns) {
119+
throw new Error('Swap transactions are required')
120+
}
121+
if (config.deflexTxns.length === 0) {
122+
throw new Error('Swap transactions array cannot be empty')
123+
}
124+
if (!config.algorand) {
125+
throw new Error('AlgorandClient instance is required')
126+
}
127+
114128
this.quote = config.quote
115129
this.deflexTxns = config.deflexTxns
116130
this.algorand = config.algorand

tests/client.test.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AlgorandClient } from '@algorandfoundation/algokit-utils'
2+
import algosdk from 'algosdk'
23
import { describe, it, expect, beforeEach, vi } from 'vitest'
34
import { DeflexClient } from '../src/client'
45
import { Protocol } from '../src/constants'
@@ -423,15 +424,42 @@ describe('DeflexClient', () => {
423424
requiredAppOptIns: [],
424425
}
425426

427+
const validAddress =
428+
'5BPCE3UNCPAIONAOMY4CVUXNU27SOCXYE4QSXEQFYXV6ORFQIKVTOR6ZTM'
429+
430+
const mockTransaction =
431+
algosdk.makePaymentTxnWithSuggestedParamsFromObject({
432+
sender: validAddress,
433+
receiver: validAddress,
434+
amount: 1000,
435+
suggestedParams: {
436+
fee: 1000,
437+
firstValid: 1000,
438+
lastValid: 2000,
439+
genesisID: 'testnet-v1.0',
440+
genesisHash: Buffer.from(
441+
'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=',
442+
'base64',
443+
),
444+
minFee: 1000,
445+
},
446+
})
447+
426448
const mockSwapResponse: FetchSwapTxnsResponse = {
427-
txns: [],
449+
txns: [
450+
{
451+
data: Buffer.from(
452+
algosdk.encodeUnsignedTransaction(mockTransaction),
453+
).toString('base64'),
454+
group: '',
455+
logicSigBlob: false,
456+
signature: false,
457+
},
458+
],
428459
}
429460

430461
mockRequest.mockResolvedValue(mockSwapResponse)
431462

432-
const validAddress =
433-
'5BPCE3UNCPAIONAOMY4CVUXNU27SOCXYE4QSXEQFYXV6ORFQIKVTOR6ZTM'
434-
435463
const composer = await client.newSwap({
436464
quote: mockQuote as DeflexQuote,
437465
address: validAddress,

tests/composer.test.ts

Lines changed: 124 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ describe('SwapComposer', () => {
5050
})
5151
}
5252

53+
const createMockDeflexTxn = (): DeflexTransaction => ({
54+
data: Buffer.from(
55+
algosdk.encodeUnsignedTransaction(createMockTransaction()),
56+
).toString('base64'),
57+
group: '',
58+
logicSigBlob: false,
59+
signature: false,
60+
})
61+
5362
beforeEach(() => {
5463
mockAlgorand = {
5564
account: {
@@ -83,25 +92,108 @@ describe('SwapComposer', () => {
8392

8493
describe('constructor', () => {
8594
it('should create a composer with valid configuration', () => {
95+
const mockDeflexTxn: DeflexTransaction = {
96+
data: Buffer.from(
97+
algosdk.encodeUnsignedTransaction(createMockTransaction()),
98+
).toString('base64'),
99+
group: '',
100+
logicSigBlob: false,
101+
signature: false,
102+
}
103+
86104
const composer = new SwapComposer({
87105
quote: createMockQuote() as DeflexQuote,
88-
deflexTxns: [],
106+
deflexTxns: [mockDeflexTxn],
89107
algorand: mockAlgorand,
90108
address: validAddress,
91109
})
92110

93111
expect(composer).toBeInstanceOf(SwapComposer)
94112
expect(composer.getStatus()).toBe(SwapComposerStatus.BUILDING)
95-
expect(composer.count()).toBe(0)
96113
})
97114

98-
it('should throw error for invalid Algorand address', () => {
115+
it('should throw error for missing quote', () => {
116+
const mockDeflexTxn: DeflexTransaction = {
117+
data: Buffer.from(
118+
algosdk.encodeUnsignedTransaction(createMockTransaction()),
119+
).toString('base64'),
120+
group: '',
121+
logicSigBlob: false,
122+
signature: false,
123+
}
124+
125+
expect(
126+
() =>
127+
new SwapComposer({
128+
quote: null as any,
129+
deflexTxns: [mockDeflexTxn],
130+
algorand: mockAlgorand,
131+
address: validAddress,
132+
}),
133+
).toThrow('Quote is required')
134+
})
135+
136+
it('should throw error for missing swap transactions', () => {
137+
expect(
138+
() =>
139+
new SwapComposer({
140+
quote: createMockQuote() as DeflexQuote,
141+
deflexTxns: null as any,
142+
algorand: mockAlgorand,
143+
address: validAddress,
144+
}),
145+
).toThrow('Swap transactions are required')
146+
})
147+
148+
it('should throw error for empty swap transactions array', () => {
99149
expect(
100150
() =>
101151
new SwapComposer({
102152
quote: createMockQuote() as DeflexQuote,
103153
deflexTxns: [],
104154
algorand: mockAlgorand,
155+
address: validAddress,
156+
}),
157+
).toThrow('Swap transactions array cannot be empty')
158+
})
159+
160+
it('should throw error for missing AlgorandClient', () => {
161+
const mockDeflexTxn: DeflexTransaction = {
162+
data: Buffer.from(
163+
algosdk.encodeUnsignedTransaction(createMockTransaction()),
164+
).toString('base64'),
165+
group: '',
166+
logicSigBlob: false,
167+
signature: false,
168+
}
169+
170+
expect(
171+
() =>
172+
new SwapComposer({
173+
quote: createMockQuote() as DeflexQuote,
174+
deflexTxns: [mockDeflexTxn],
175+
algorand: null as any,
176+
address: validAddress,
177+
}),
178+
).toThrow('AlgorandClient instance is required')
179+
})
180+
181+
it('should throw error for invalid Algorand address', () => {
182+
const mockDeflexTxn: DeflexTransaction = {
183+
data: Buffer.from(
184+
algosdk.encodeUnsignedTransaction(createMockTransaction()),
185+
).toString('base64'),
186+
group: '',
187+
logicSigBlob: false,
188+
signature: false,
189+
}
190+
191+
expect(
192+
() =>
193+
new SwapComposer({
194+
quote: createMockQuote() as DeflexQuote,
195+
deflexTxns: [mockDeflexTxn],
196+
algorand: mockAlgorand,
105197
address: 'invalid-address',
106198
}),
107199
).toThrow('Invalid Algorand address')
@@ -112,7 +204,7 @@ describe('SwapComposer', () => {
112204
it('should return BUILDING status initially', () => {
113205
const composer = new SwapComposer({
114206
quote: createMockQuote() as DeflexQuote,
115-
deflexTxns: [],
207+
deflexTxns: [createMockDeflexTxn()],
116208
algorand: mockAlgorand,
117209
address: validAddress,
118210
})
@@ -125,7 +217,7 @@ describe('SwapComposer', () => {
125217
it('should return 0 for empty composer', () => {
126218
const composer = new SwapComposer({
127219
quote: createMockQuote() as DeflexQuote,
128-
deflexTxns: [],
220+
deflexTxns: [createMockDeflexTxn()],
129221
algorand: mockAlgorand,
130222
address: validAddress,
131223
})
@@ -136,7 +228,7 @@ describe('SwapComposer', () => {
136228
it('should return correct count after adding transactions', () => {
137229
const composer = new SwapComposer({
138230
quote: createMockQuote() as DeflexQuote,
139-
deflexTxns: [],
231+
deflexTxns: [createMockDeflexTxn()],
140232
algorand: mockAlgorand,
141233
address: validAddress,
142234
})
@@ -153,7 +245,7 @@ describe('SwapComposer', () => {
153245
it('should add a transaction to the group', () => {
154246
const composer = new SwapComposer({
155247
quote: createMockQuote() as DeflexQuote,
156-
deflexTxns: [],
248+
deflexTxns: [createMockDeflexTxn()],
157249
algorand: mockAlgorand,
158250
address: validAddress,
159251
})
@@ -167,7 +259,7 @@ describe('SwapComposer', () => {
167259
it('should allow chaining', () => {
168260
const composer = new SwapComposer({
169261
quote: createMockQuote() as DeflexQuote,
170-
deflexTxns: [],
262+
deflexTxns: [createMockDeflexTxn()],
171263
algorand: mockAlgorand,
172264
address: validAddress,
173265
})
@@ -183,7 +275,7 @@ describe('SwapComposer', () => {
183275
it('should throw error when not in BUILDING status', async () => {
184276
const composer = new SwapComposer({
185277
quote: createMockQuote() as DeflexQuote,
186-
deflexTxns: [],
278+
deflexTxns: [createMockDeflexTxn()],
187279
algorand: mockAlgorand,
188280
address: validAddress,
189281
})
@@ -208,7 +300,7 @@ describe('SwapComposer', () => {
208300
it('should throw error when exceeding max group size', () => {
209301
const composer = new SwapComposer({
210302
quote: createMockQuote() as DeflexQuote,
211-
deflexTxns: [],
303+
deflexTxns: [createMockDeflexTxn()],
212304
algorand: mockAlgorand,
213305
address: validAddress,
214306
})
@@ -364,7 +456,7 @@ describe('SwapComposer', () => {
364456
it('should sign transactions and return signed blobs', async () => {
365457
const composer = new SwapComposer({
366458
quote: createMockQuote() as DeflexQuote,
367-
deflexTxns: [],
459+
deflexTxns: [createMockDeflexTxn()],
368460
algorand: mockAlgorand,
369461
address: validAddress,
370462
})
@@ -380,7 +472,7 @@ describe('SwapComposer', () => {
380472
},
381473
)
382474

383-
expect(signedTxns).toHaveLength(1)
475+
expect(signedTxns).toHaveLength(2) // 1 user txn + 1 from deflexTxns
384476
expect(signedTxns?.[0]).toBeInstanceOf(Uint8Array)
385477
expect(composer.getStatus()).toBe(SwapComposerStatus.SIGNED)
386478
})
@@ -417,7 +509,7 @@ describe('SwapComposer', () => {
417509
it('should return cached signed transactions on subsequent calls', async () => {
418510
const composer = new SwapComposer({
419511
quote: createMockQuote() as DeflexQuote,
420-
deflexTxns: [],
512+
deflexTxns: [createMockDeflexTxn()],
421513
algorand: mockAlgorand,
422514
address: validAddress,
423515
})
@@ -488,7 +580,7 @@ describe('SwapComposer', () => {
488580
it('should assign group ID to transactions', async () => {
489581
const composer = new SwapComposer({
490582
quote: createMockQuote() as DeflexQuote,
491-
deflexTxns: [],
583+
deflexTxns: [createMockDeflexTxn()],
492584
algorand: mockAlgorand,
493585
address: validAddress,
494586
})
@@ -516,7 +608,7 @@ describe('SwapComposer', () => {
516608
it('should submit signed transactions to the network', async () => {
517609
const composer = new SwapComposer({
518610
quote: createMockQuote() as DeflexQuote,
519-
deflexTxns: [],
611+
deflexTxns: [createMockDeflexTxn()],
520612
algorand: mockAlgorand,
521613
address: validAddress,
522614
})
@@ -532,15 +624,15 @@ describe('SwapComposer', () => {
532624
},
533625
)
534626

535-
expect(txIds).toHaveLength(1)
627+
expect(txIds).toHaveLength(2) // 1 user txn + 1 from deflexTxns
536628
expect(composer.getStatus()).toBe(SwapComposerStatus.SUBMITTED)
537629
expect(mockAlgorand.client.algod.sendRawTransaction).toHaveBeenCalled()
538630
})
539631

540632
it('should throw error when trying to resubmit', async () => {
541633
const composer = new SwapComposer({
542634
quote: createMockQuote() as DeflexQuote,
543-
deflexTxns: [],
635+
deflexTxns: [createMockDeflexTxn()],
544636
algorand: mockAlgorand,
545637
address: validAddress,
546638
})
@@ -576,7 +668,7 @@ describe('SwapComposer', () => {
576668
it('should execute the swap and wait for confirmation', async () => {
577669
const composer = new SwapComposer({
578670
quote: createMockQuote() as DeflexQuote,
579-
deflexTxns: [],
671+
deflexTxns: [createMockDeflexTxn()],
580672
algorand: mockAlgorand,
581673
address: validAddress,
582674
})
@@ -593,14 +685,14 @@ describe('SwapComposer', () => {
593685
)
594686

595687
expect(result.confirmedRound).toBe(1234n)
596-
expect(result.txIds).toHaveLength(1)
688+
expect(result.txIds).toHaveLength(2) // 1 user txn + 1 from deflexTxns
597689
expect(composer.getStatus()).toBe(SwapComposerStatus.COMMITTED)
598690
})
599691

600692
it('should throw error when already committed', async () => {
601693
const composer = new SwapComposer({
602694
quote: createMockQuote() as DeflexQuote,
603-
deflexTxns: [],
695+
deflexTxns: [createMockDeflexTxn()],
604696
algorand: mockAlgorand,
605697
address: validAddress,
606698
})
@@ -629,7 +721,7 @@ describe('SwapComposer', () => {
629721
it('should use custom wait rounds parameter', async () => {
630722
const composer = new SwapComposer({
631723
quote: createMockQuote() as DeflexQuote,
632-
deflexTxns: [],
724+
deflexTxns: [createMockDeflexTxn()],
633725
algorand: mockAlgorand,
634726
address: validAddress,
635727
})
@@ -649,7 +741,7 @@ describe('SwapComposer', () => {
649741
// Verify execution completed successfully
650742
expect(composer.getStatus()).toBe(SwapComposerStatus.COMMITTED)
651743
expect(result.confirmedRound).toBe(1234n)
652-
expect(result.txIds).toHaveLength(1)
744+
expect(result.txIds).toHaveLength(2) // 1 user txn + 1 from deflexTxns
653745
})
654746
})
655747

@@ -895,17 +987,16 @@ describe('SwapComposer', () => {
895987
)
896988
})
897989

898-
it('should handle empty transaction array', async () => {
899-
const composer = new SwapComposer({
900-
quote: createMockQuote() as DeflexQuote,
901-
deflexTxns: [],
902-
algorand: mockAlgorand,
903-
address: validAddress,
904-
})
905-
906-
await composer.addSwapTransactions()
907-
908-
expect(composer.count()).toBe(0)
990+
it('should throw error for empty transaction array', () => {
991+
expect(
992+
() =>
993+
new SwapComposer({
994+
quote: createMockQuote() as DeflexQuote,
995+
deflexTxns: [],
996+
algorand: mockAlgorand,
997+
address: validAddress,
998+
}),
999+
).toThrow('Swap transactions array cannot be empty')
9091000
})
9101001
})
9111002

0 commit comments

Comments
 (0)