Skip to content

Commit 291cd9b

Browse files
committed
feat: added more test coverage for predicates
1 parent a6d5a7b commit 291cd9b

3 files changed

Lines changed: 390 additions & 39 deletions

File tree

src/core/contract/contract.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,126 @@ describe('contract — write (ERC20 on Base Sepolia)', () => {
243243
});
244244
});
245245

246+
// ---------------------------------------------------------------------------
247+
// contract — check
248+
// ---------------------------------------------------------------------------
249+
250+
describe('contract — check (ERC20 on Base Sepolia)', () => {
251+
const token = contract(publicClient, USDC, ERC20_ABI);
252+
253+
it('check(balanceOf) returns a ComposableCall with a functionSig', () => {
254+
const call = token.check({
255+
functionName: 'balanceOf',
256+
args: [WETH],
257+
constraints: [{ gte: 0n }],
258+
});
259+
expect(typeof call.functionSig).toBe('string');
260+
expect(call.functionSig.length).toBeGreaterThan(0);
261+
});
262+
263+
it('check(balanceOf) encodes the correct function selector', () => {
264+
const call = token.check({
265+
functionName: 'balanceOf',
266+
args: [WETH],
267+
constraints: [{ gte: 0n }],
268+
});
269+
// keccak256("balanceOf(address)") first 4 bytes = 0x70a08231
270+
expect(call.functionSig).toBe('0x70a08231');
271+
});
272+
273+
it('check(totalSupply) encodes the correct function selector', () => {
274+
const call = token.check({ functionName: 'totalSupply', args: [], constraints: [{ gte: 1n }] });
275+
// keccak256("totalSupply()") first 4 bytes = 0x18160ddd
276+
expect(call.functionSig).toBe('0x18160ddd');
277+
});
278+
279+
it('check(balanceOf) and check(totalSupply) produce different functionSigs', () => {
280+
const a = token.check({ functionName: 'balanceOf', args: [WETH], constraints: [{ gte: 0n }] });
281+
const b = token.check({ functionName: 'totalSupply', args: [], constraints: [{ gte: 0n }] });
282+
expect(a.functionSig).not.toBe(b.functionSig);
283+
});
284+
285+
it('check(balanceOf) outputParams is empty', () => {
286+
const call = token.check({
287+
functionName: 'balanceOf',
288+
args: [WETH],
289+
constraints: [{ gte: 0n }],
290+
});
291+
expect(call.outputParams).toHaveLength(0);
292+
});
293+
294+
it('check(balanceOf) inputParams contains a STATIC_CALL param', () => {
295+
const call = token.check({
296+
functionName: 'balanceOf',
297+
args: [WETH],
298+
constraints: [{ gte: 0n }],
299+
});
300+
const staticCallParam = call.inputParams.find(
301+
(p) => p.fetcherType === InputParamFetcherType.STATIC_CALL,
302+
);
303+
expect(staticCallParam).toBeDefined();
304+
});
305+
306+
it('one constraint is applied to the STATIC_CALL param', () => {
307+
const call = token.check({
308+
functionName: 'balanceOf',
309+
args: [WETH],
310+
constraints: [{ gte: 1_000n }],
311+
});
312+
const staticCallParam = call.inputParams.find(
313+
(p) => p.fetcherType === InputParamFetcherType.STATIC_CALL,
314+
);
315+
expect(staticCallParam?.constraints).toHaveLength(1);
316+
});
317+
318+
it('multiple constraints are all applied to the STATIC_CALL param', () => {
319+
const call = token.check({
320+
functionName: 'balanceOf',
321+
args: [WETH],
322+
constraints: [{ gte: 1_000n }, { lte: 1_000_000n }],
323+
});
324+
const staticCallParam = call.inputParams.find(
325+
(p) => p.fetcherType === InputParamFetcherType.STATIC_CALL,
326+
);
327+
expect(staticCallParam?.constraints).toHaveLength(2);
328+
});
329+
330+
it('constraints do not affect paramData of the STATIC_CALL param', () => {
331+
const withConstraint = token.check({
332+
functionName: 'balanceOf',
333+
args: [WETH],
334+
constraints: [{ gte: 999n }],
335+
});
336+
const withOtherConstraint = token.check({
337+
functionName: 'balanceOf',
338+
args: [WETH],
339+
constraints: [{ lte: 1n }],
340+
});
341+
const staticA = withConstraint.inputParams.find(
342+
(p) => p.fetcherType === InputParamFetcherType.STATIC_CALL,
343+
);
344+
const staticB = withOtherConstraint.inputParams.find(
345+
(p) => p.fetcherType === InputParamFetcherType.STATIC_CALL,
346+
);
347+
expect(staticA?.paramData).toBe(staticB?.paramData);
348+
});
349+
350+
it('check(balanceOf) produces different paramData for different addresses', () => {
351+
const a = token.check({ functionName: 'balanceOf', args: [USDC], constraints: [{ gte: 0n }] });
352+
const b = token.check({ functionName: 'balanceOf', args: [WETH], constraints: [{ gte: 0n }] });
353+
const staticA = a.inputParams.find((p) => p.fetcherType === InputParamFetcherType.STATIC_CALL);
354+
const staticB = b.inputParams.find((p) => p.fetcherType === InputParamFetcherType.STATIC_CALL);
355+
expect(staticA?.paramData).not.toBe(staticB?.paramData);
356+
});
357+
358+
it('check(balanceOf) is deterministic for the same args', () => {
359+
const a = token.check({ functionName: 'balanceOf', args: [WETH], constraints: [{ gte: 0n }] });
360+
const b = token.check({ functionName: 'balanceOf', args: [WETH], constraints: [{ gte: 0n }] });
361+
expect(a.functionSig).toBe(b.functionSig);
362+
expect(JSON.stringify(a.inputParams)).toBe(JSON.stringify(b.inputParams));
363+
});
364+
});
365+
246366
// ---------------------------------------------------------------------------
247367
// contract — runtimeValue
248368
// ---------------------------------------------------------------------------

src/core/token/token.test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,167 @@ describe('ERC20Token — runtimeAllowance with constraints', () => {
303303
// ERC20Token — write
304304
// ---------------------------------------------------------------------------
305305

306+
// ---------------------------------------------------------------------------
307+
// ERC20Token — check
308+
// ---------------------------------------------------------------------------
309+
310+
describe('ERC20Token — check', () => {
311+
const usdc = ERC20Token(publicClient, USDC_ADDRESS);
312+
313+
it('check(balanceOf) returns a ComposableCall with a functionSig', () => {
314+
const call = usdc.check({
315+
functionName: 'balanceOf',
316+
args: [UNISWAP_V3_ROUTER],
317+
constraints: [{ gte: 0n }],
318+
});
319+
expect(typeof call.functionSig).toBe('string');
320+
expect(call.functionSig.length).toBeGreaterThan(0);
321+
});
322+
323+
it('check(balanceOf) encodes the correct function selector', () => {
324+
const call = usdc.check({
325+
functionName: 'balanceOf',
326+
args: [UNISWAP_V3_ROUTER],
327+
constraints: [{ gte: 0n }],
328+
});
329+
// keccak256("balanceOf(address)") first 4 bytes = 0x70a08231
330+
expect(call.functionSig).toBe('0x70a08231');
331+
});
332+
333+
it('check(allowance) encodes the correct function selector', () => {
334+
const call = usdc.check({
335+
functionName: 'allowance',
336+
args: [UNISWAP_V3_ROUTER, WETH_ADDRESS],
337+
constraints: [{ gte: 0n }],
338+
});
339+
// keccak256("allowance(address,address)") first 4 bytes = 0xdd62ed3e
340+
expect(call.functionSig).toBe('0xdd62ed3e');
341+
});
342+
343+
it('check(balanceOf) and check(allowance) produce different functionSigs', () => {
344+
const a = usdc.check({
345+
functionName: 'balanceOf',
346+
args: [UNISWAP_V3_ROUTER],
347+
constraints: [{ gte: 0n }],
348+
});
349+
const b = usdc.check({
350+
functionName: 'allowance',
351+
args: [UNISWAP_V3_ROUTER, WETH_ADDRESS],
352+
constraints: [{ gte: 0n }],
353+
});
354+
expect(a.functionSig).not.toBe(b.functionSig);
355+
});
356+
357+
it('check(balanceOf) outputParams is empty', () => {
358+
const call = usdc.check({
359+
functionName: 'balanceOf',
360+
args: [UNISWAP_V3_ROUTER],
361+
constraints: [{ gte: 0n }],
362+
});
363+
expect(call.outputParams).toHaveLength(0);
364+
});
365+
366+
it('check(balanceOf) inputParams contains a STATIC_CALL param', () => {
367+
const call = usdc.check({
368+
functionName: 'balanceOf',
369+
args: [UNISWAP_V3_ROUTER],
370+
constraints: [{ gte: 0n }],
371+
});
372+
const staticCallParam = call.inputParams.find(
373+
(p) => p.fetcherType === InputParamFetcherType.STATIC_CALL,
374+
);
375+
expect(staticCallParam).toBeDefined();
376+
});
377+
378+
it('one constraint is applied to the STATIC_CALL param', () => {
379+
const call = usdc.check({
380+
functionName: 'balanceOf',
381+
args: [UNISWAP_V3_ROUTER],
382+
constraints: [{ gte: 1_000n }],
383+
});
384+
const staticCallParam = call.inputParams.find(
385+
(p) => p.fetcherType === InputParamFetcherType.STATIC_CALL,
386+
);
387+
expect(staticCallParam?.constraints).toHaveLength(1);
388+
});
389+
390+
it('multiple constraints are all applied to the STATIC_CALL param', () => {
391+
const call = usdc.check({
392+
functionName: 'balanceOf',
393+
args: [UNISWAP_V3_ROUTER],
394+
constraints: [{ gte: 1_000n }, { lte: 1_000_000n }],
395+
});
396+
const staticCallParam = call.inputParams.find(
397+
(p) => p.fetcherType === InputParamFetcherType.STATIC_CALL,
398+
);
399+
expect(staticCallParam?.constraints).toHaveLength(2);
400+
});
401+
402+
it('constraints do not affect paramData of the STATIC_CALL param', () => {
403+
const a = usdc.check({
404+
functionName: 'balanceOf',
405+
args: [UNISWAP_V3_ROUTER],
406+
constraints: [{ gte: 999n }],
407+
});
408+
const b = usdc.check({
409+
functionName: 'balanceOf',
410+
args: [UNISWAP_V3_ROUTER],
411+
constraints: [{ lte: 1n }],
412+
});
413+
const staticA = a.inputParams.find((p) => p.fetcherType === InputParamFetcherType.STATIC_CALL);
414+
const staticB = b.inputParams.find((p) => p.fetcherType === InputParamFetcherType.STATIC_CALL);
415+
expect(staticA?.paramData).toBe(staticB?.paramData);
416+
});
417+
418+
it('check(balanceOf) produces different paramData for different addresses', () => {
419+
const a = usdc.check({
420+
functionName: 'balanceOf',
421+
args: [UNISWAP_V3_ROUTER],
422+
constraints: [{ gte: 0n }],
423+
});
424+
const b = usdc.check({
425+
functionName: 'balanceOf',
426+
args: [WETH_ADDRESS],
427+
constraints: [{ gte: 0n }],
428+
});
429+
const staticA = a.inputParams.find((p) => p.fetcherType === InputParamFetcherType.STATIC_CALL);
430+
const staticB = b.inputParams.find((p) => p.fetcherType === InputParamFetcherType.STATIC_CALL);
431+
expect(staticA?.paramData).not.toBe(staticB?.paramData);
432+
});
433+
434+
it('check(balanceOf) is deterministic for the same args', () => {
435+
const a = usdc.check({
436+
functionName: 'balanceOf',
437+
args: [UNISWAP_V3_ROUTER],
438+
constraints: [{ gte: 0n }],
439+
});
440+
const b = usdc.check({
441+
functionName: 'balanceOf',
442+
args: [UNISWAP_V3_ROUTER],
443+
constraints: [{ gte: 0n }],
444+
});
445+
expect(a.functionSig).toBe(b.functionSig);
446+
expect(JSON.stringify(a.inputParams)).toBe(JSON.stringify(b.inputParams));
447+
});
448+
449+
it('check on WETH produces different paramData than check on USDC for same owner', () => {
450+
const weth = ERC20Token(publicClient, WETH_ADDRESS);
451+
const a = usdc.check({
452+
functionName: 'balanceOf',
453+
args: [UNISWAP_V3_ROUTER],
454+
constraints: [{ gte: 0n }],
455+
});
456+
const b = weth.check({
457+
functionName: 'balanceOf',
458+
args: [UNISWAP_V3_ROUTER],
459+
constraints: [{ gte: 0n }],
460+
});
461+
const staticA = a.inputParams.find((p) => p.fetcherType === InputParamFetcherType.STATIC_CALL);
462+
const staticB = b.inputParams.find((p) => p.fetcherType === InputParamFetcherType.STATIC_CALL);
463+
expect(staticA?.paramData).not.toBe(staticB?.paramData);
464+
});
465+
});
466+
306467
describe('ERC20Token — write', () => {
307468
const usdc = ERC20Token(publicClient, USDC_ADDRESS);
308469

0 commit comments

Comments
 (0)