Skip to content

Commit 03214f0

Browse files
committed
add missing tests
1 parent c125a01 commit 03214f0

14 files changed

+471
-140
lines changed

.github/workflows/unit_tests.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242

4343
test-agentkit-typescript:
4444
runs-on: ubuntu-latest
45+
timeout-minutes: 15
4546
strategy:
4647
matrix:
4748
node-version: ["18", "20"]
@@ -56,4 +57,4 @@ jobs:
5657
working-directory: ./typescript
5758
run: |
5859
npm ci
59-
npm run test
60+
npm run test -- -- --testTimeout=300000

typescript/agentkit/src/action-providers/safe/safeApiActionProvider.test.ts

+294-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ describe("Safe API Action Provider", () => {
4343
let mockSafeApiKit: jest.Mocked<SafeApiKit>;
4444

4545
const MOCK_SAFE_ADDRESS = "0xe6b2af36b3bb8d47206a129ff11d5a2de2a63c83";
46-
const MOCK_NETWORK = "base-sepolia";
46+
const MOCK_DELEGATE_ADDRESS = "0x1234567890123456789012345678901234567890";
47+
const MOCK_TOKEN_ADDRESS = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd";
48+
const MOCK_NETWORK = "ethereum-sepolia";
4749
const MOCK_BALANCE = BigInt(1000000000000000000); // 1 ETH in wei
4850

4951
beforeEach(() => {
@@ -144,4 +146,295 @@ describe("Safe API Action Provider", () => {
144146
expect(result).toBe(false);
145147
});
146148
});
149+
150+
describe("getAllowanceInfo", () => {
151+
beforeEach(() => {
152+
// Mock additional wallet methods needed for getAllowanceInfo
153+
mockWallet.readContract = jest.fn();
154+
155+
// Mock the getAllowanceModuleDeployment function
156+
jest.mock("@safe-global/safe-modules-deployments", () => ({
157+
getAllowanceModuleDeployment: jest.fn().mockReturnValue({
158+
networkAddresses: { "421614": "0xallowanceModuleAddress" },
159+
abi: [
160+
{
161+
name: "getTokens",
162+
type: "function",
163+
inputs: [{ type: "address" }, { type: "address" }],
164+
outputs: [{ type: "address[]" }],
165+
},
166+
{
167+
name: "getTokenAllowance",
168+
type: "function",
169+
inputs: [{ type: "address" }, { type: "address" }, { type: "address" }],
170+
outputs: [
171+
{ type: "uint256" }, // amount
172+
{ type: "uint256" }, // spent
173+
{ type: "uint256" }, // resetTimeMin
174+
{ type: "uint256" }, // lastResetMin
175+
{ type: "uint256" }, // nonce
176+
],
177+
},
178+
],
179+
}),
180+
}));
181+
});
182+
183+
it("should successfully get allowance info", async () => {
184+
// Mock token list response
185+
(mockWallet.readContract as jest.Mock).mockImplementation(params => {
186+
if (params.functionName === "getTokens") {
187+
return [MOCK_TOKEN_ADDRESS];
188+
} else if (params.functionName === "getTokenAllowance") {
189+
return [
190+
BigInt(1000000000000000000), // amount: 1 token
191+
BigInt(300000000000000000), // spent: 0.3 token
192+
BigInt(1440), // resetTimeMin: 24 hours
193+
BigInt(Math.floor(Date.now() / (60 * 1000)) - 720), // lastResetMin: 12 hours ago
194+
BigInt(1), // nonce
195+
];
196+
} else if (params.functionName === "symbol") {
197+
return "TEST";
198+
} else if (params.functionName === "decimals") {
199+
return 18;
200+
} else if (params.functionName === "balanceOf") {
201+
return BigInt(5000000000000000000); // 5 tokens
202+
}
203+
});
204+
205+
const args = {
206+
safeAddress: MOCK_SAFE_ADDRESS,
207+
delegateAddress: MOCK_DELEGATE_ADDRESS,
208+
};
209+
210+
const response = await actionProvider.getAllowanceInfo(mockWallet, args);
211+
212+
// Verify response contains expected information
213+
expect(response).toContain(`Delegate ${MOCK_DELEGATE_ADDRESS} has the following allowances`);
214+
expect(response).toContain(`TEST (${MOCK_TOKEN_ADDRESS})`);
215+
expect(response).toContain("Current Safe balance: 5 TEST");
216+
expect(response).toContain("Allowance: 0.7 available of 1 total (0.3 spent)");
217+
expect(response).toContain("resets every 1440 minutes");
218+
});
219+
220+
it("should handle case with no allowances", async () => {
221+
// Mock empty token list response
222+
(mockWallet.readContract as jest.Mock).mockImplementation(params => {
223+
if (params.functionName === "getTokens") {
224+
return []; // No tokens with allowances
225+
}
226+
});
227+
228+
const args = {
229+
safeAddress: MOCK_SAFE_ADDRESS,
230+
delegateAddress: MOCK_DELEGATE_ADDRESS,
231+
};
232+
233+
const response = await actionProvider.getAllowanceInfo(mockWallet, args);
234+
235+
// Verify response indicates no allowances
236+
expect(response).toBe(
237+
`Get allowance: Delegate ${MOCK_DELEGATE_ADDRESS} has no token allowances from Safe ${MOCK_SAFE_ADDRESS}`,
238+
);
239+
});
240+
241+
it("should handle errors when getting allowance info", async () => {
242+
// Mock error when reading contract
243+
const error = new Error("Failed to get allowance info");
244+
(mockWallet.readContract as jest.Mock).mockRejectedValue(error);
245+
246+
const args = {
247+
safeAddress: MOCK_SAFE_ADDRESS,
248+
delegateAddress: MOCK_DELEGATE_ADDRESS,
249+
};
250+
251+
const response = await actionProvider.getAllowanceInfo(mockWallet, args);
252+
expect(response).toBe(`Get allowance: Error getting allowance: ${error.message}`);
253+
});
254+
});
255+
256+
describe("withdrawAllowance", () => {
257+
beforeEach(() => {
258+
// Mock wallet methods needed for withdrawAllowance
259+
mockWallet.readContract = jest.fn();
260+
mockWallet.signHash = jest.fn();
261+
mockWallet.sendTransaction = jest.fn();
262+
mockWallet.waitForTransactionReceipt = jest.fn();
263+
264+
// Mock the getAllowanceModuleDeployment function
265+
jest.mock("@safe-global/safe-modules-deployments", () => ({
266+
getAllowanceModuleDeployment: jest.fn().mockReturnValue({
267+
networkAddresses: { "11155111": "0xallowanceModuleAddress" }, // Sepolia chain ID
268+
abi: [
269+
{
270+
name: "getTokenAllowance",
271+
type: "function",
272+
inputs: [{ type: "address" }, { type: "address" }, { type: "address" }],
273+
outputs: [
274+
{ type: "uint256" }, // amount
275+
{ type: "uint256" }, // spent
276+
{ type: "uint256" }, // resetTimeMin
277+
{ type: "uint256" }, // lastResetMin
278+
{ type: "uint256" }, // nonce
279+
],
280+
},
281+
{
282+
name: "generateTransferHash",
283+
type: "function",
284+
inputs: [
285+
{ type: "address" }, // safe
286+
{ type: "address" }, // token
287+
{ type: "address" }, // to
288+
{ type: "uint256" }, // amount
289+
{ type: "address" }, // paymentToken
290+
{ type: "uint256" }, // payment
291+
{ type: "uint256" }, // nonce
292+
],
293+
outputs: [{ type: "bytes32" }],
294+
},
295+
{
296+
name: "executeAllowanceTransfer",
297+
type: "function",
298+
inputs: [
299+
{ type: "address" }, // safe
300+
{ type: "address" }, // token
301+
{ type: "address" }, // to
302+
{ type: "uint256" }, // amount
303+
{ type: "address" }, // paymentToken
304+
{ type: "uint256" }, // payment
305+
{ type: "address" }, // delegate
306+
{ type: "bytes" }, // signature
307+
],
308+
outputs: [],
309+
},
310+
],
311+
}),
312+
}));
313+
});
314+
315+
it("should successfully withdraw tokens using allowance", async () => {
316+
// Mock contract read responses
317+
(mockWallet.readContract as jest.Mock).mockImplementation(params => {
318+
if (params.functionName === "getTokenAllowance") {
319+
return [
320+
BigInt(5000000000000000000), // amount: 5 tokens
321+
BigInt(1000000000000000000), // spent: 1 token
322+
BigInt(0), // resetTimeMin: no reset
323+
BigInt(0), // lastResetMin: no reset
324+
BigInt(3), // nonce: 3
325+
];
326+
} else if (params.functionName === "generateTransferHash") {
327+
return "0xmockhash123456789";
328+
} else if (params.functionName === "decimals") {
329+
return 18;
330+
} else if (params.functionName === "symbol") {
331+
return "TEST";
332+
}
333+
});
334+
335+
// Mock signature
336+
(mockWallet.signHash as jest.Mock).mockResolvedValue("0xmocksignature");
337+
338+
// Mock transaction sending
339+
const mockTxHash = "0xmocktxhash123456789";
340+
(mockWallet.sendTransaction as jest.Mock).mockResolvedValue(mockTxHash);
341+
342+
// Mock transaction receipt
343+
(mockWallet.waitForTransactionReceipt as jest.Mock).mockResolvedValue({
344+
transactionHash: mockTxHash,
345+
status: "success",
346+
});
347+
348+
const args = {
349+
safeAddress: MOCK_SAFE_ADDRESS,
350+
delegateAddress: MOCK_DELEGATE_ADDRESS,
351+
tokenAddress: MOCK_TOKEN_ADDRESS,
352+
amount: "2.5", // 2.5 tokens
353+
};
354+
355+
const response = await actionProvider.withdrawAllowance(mockWallet, args);
356+
357+
// Verify the response contains expected information
358+
expect(response).toContain(`Successfully withdrew 2.5 TEST from Safe ${MOCK_SAFE_ADDRESS}`);
359+
expect(response).toContain(`Transaction hash: ${mockTxHash}`);
360+
361+
// Verify the correct contract methods were called
362+
expect(mockWallet.readContract).toHaveBeenCalledWith(
363+
expect.objectContaining({
364+
functionName: "getTokenAllowance",
365+
args: [MOCK_SAFE_ADDRESS, MOCK_DELEGATE_ADDRESS, MOCK_TOKEN_ADDRESS],
366+
}),
367+
);
368+
369+
expect(mockWallet.readContract).toHaveBeenCalledWith(
370+
expect.objectContaining({
371+
functionName: "generateTransferHash",
372+
}),
373+
);
374+
375+
expect(mockWallet.signHash).toHaveBeenCalledWith("0xmockhash123456789");
376+
377+
expect(mockWallet.sendTransaction).toHaveBeenCalledWith(
378+
expect.objectContaining({
379+
to: expect.any(String),
380+
data: expect.any(String),
381+
value: BigInt(0),
382+
}),
383+
);
384+
});
385+
386+
it("should handle errors when withdrawing allowance", async () => {
387+
// Mock error when reading contract
388+
const error = new Error("Insufficient allowance");
389+
(mockWallet.readContract as jest.Mock).mockRejectedValue(error);
390+
391+
const args = {
392+
safeAddress: MOCK_SAFE_ADDRESS,
393+
delegateAddress: MOCK_DELEGATE_ADDRESS,
394+
tokenAddress: MOCK_TOKEN_ADDRESS,
395+
amount: "10", // 10 tokens
396+
};
397+
398+
const response = await actionProvider.withdrawAllowance(mockWallet, args);
399+
expect(response).toBe(`Withdraw allowance: Error withdrawing allowance: ${error.message}`);
400+
});
401+
402+
it("should handle transaction failure", async () => {
403+
// Mock successful contract reads
404+
(mockWallet.readContract as jest.Mock).mockImplementation(params => {
405+
if (params.functionName === "getTokenAllowance") {
406+
return [
407+
BigInt(5000000000000000000), // amount: 5 tokens
408+
BigInt(1000000000000000000), // spent: 1 token
409+
BigInt(0), // resetTimeMin: no reset
410+
BigInt(0), // lastResetMin: no reset
411+
BigInt(3), // nonce: 3
412+
];
413+
} else if (params.functionName === "generateTransferHash") {
414+
return "0xmockhash123456789";
415+
} else if (params.functionName === "decimals") {
416+
return 18;
417+
} else if (params.functionName === "symbol") {
418+
return "TEST";
419+
}
420+
});
421+
422+
// Mock signature
423+
(mockWallet.signHash as jest.Mock).mockResolvedValue("0xmocksignature");
424+
425+
// Mock transaction sending failure
426+
const txError = new Error("Transaction reverted");
427+
(mockWallet.sendTransaction as jest.Mock).mockRejectedValue(txError);
428+
429+
const args = {
430+
safeAddress: MOCK_SAFE_ADDRESS,
431+
delegateAddress: MOCK_DELEGATE_ADDRESS,
432+
tokenAddress: MOCK_TOKEN_ADDRESS,
433+
amount: "2.5", // 2.5 tokens
434+
};
435+
436+
const response = await actionProvider.withdrawAllowance(mockWallet, args);
437+
expect(response).toBe(`Withdraw allowance: Error withdrawing allowance: ${txError.message}`);
438+
});
439+
});
147440
});

0 commit comments

Comments
 (0)