Skip to content

Commit 1544efb

Browse files
committed
Merge branch 'develop' of https://github.com/elizaOS/eliza into shaw/more-cache-toolcalling
2 parents ad7798a + d1f8f08 commit 1544efb

21 files changed

Lines changed: 1016 additions & 61 deletions

File tree

.github/workflows/benchmark-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
matrix:
3030
include:
3131
- lane: benchmark-tests
32-
command: bunx vitest run packages/app-core/src/benchmark/*.test.ts --passWithNoTests
32+
command: bunx vitest run --config packages/app-core/vitest.config.ts packages/app-core/src/benchmark/*.test.ts --passWithNoTests
3333
- lane: benchmark-lint
3434
command: bunx @biomejs/biome check packages/app-core/src/benchmark
3535

cloud/apps/api/test/e2e/group-l-app-charges.test.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ describe("App charge requests", () => {
6363
expect(res.status).toBe(401);
6464
});
6565

66-
test("happy path: creates a one dollar card/crypto charge with callback metadata", async () => {
66+
test("happy path: creates a five dollar card/crypto charge with callback metadata", async () => {
6767
if (!shouldRunAuthed()) return;
6868
const appId = await createTestApp();
6969

7070
const res = await api.post(
7171
`/api/v1/apps/${appId}/charges`,
7272
{
73-
amount: 1,
74-
description: "Test $1 payment request",
73+
amount: 5,
74+
description: "Agent says: sure, please send me $5",
7575
providers: ["stripe", "oxapay"],
7676
callback_url: "https://example.com/payment-callback",
7777
callback_secret: "test-callback-secret",
@@ -103,7 +103,7 @@ describe("App charge requests", () => {
103103

104104
expect(body.success).toBe(true);
105105
expect(body.charge?.appId).toBe(appId);
106-
expect(body.charge?.amountUsd).toBe(1);
106+
expect(body.charge?.amountUsd).toBe(5);
107107
expect(body.charge?.status).toBe("requested");
108108
expect(body.charge?.providers).toEqual(["stripe", "oxapay"]);
109109
expect(body.charge?.paymentUrl).toContain(`/payment/app-charge/${appId}/`);
@@ -114,9 +114,22 @@ describe("App charge requests", () => {
114114
expect(publicRes.status).toBe(200);
115115
const publicBody = (await publicRes.json()) as {
116116
charge?: { amountUsd?: number; metadata?: Record<string, unknown> };
117+
app?: { id?: string; name?: string };
117118
};
118-
expect(publicBody.charge?.amountUsd).toBe(1);
119+
expect(publicBody.charge?.amountUsd).toBe(5);
120+
expect(publicBody.app?.id).toBe(appId);
119121
expect(publicBody.charge?.metadata?.callback_secret).toBeUndefined();
122+
123+
const listRes = await api.get(`/api/v1/apps/${appId}/charges?limit=5`, {
124+
headers: bearerHeaders(),
125+
});
126+
expect(listRes.status).toBe(200);
127+
const listBody = (await listRes.json()) as {
128+
charges?: Array<{ id?: string; amountUsd?: number; paymentUrl?: string }>;
129+
};
130+
const listed = listBody.charges?.find((charge) => charge.id === body.charge?.id);
131+
expect(listed?.amountUsd).toBe(5);
132+
expect(listed?.paymentUrl).toBe(body.charge?.paymentUrl);
120133
});
121134

122135
test("validation: rejects charges below one dollar", async () => {
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import "@testing-library/jest-dom/vitest";
2+
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
3+
import { MemoryRouter, Route, Routes } from "react-router-dom";
4+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
5+
6+
const mocks = vi.hoisted(() => ({
7+
api: vi.fn(),
8+
useSessionAuth: vi.fn(),
9+
locationAssign: vi.fn(),
10+
}));
11+
12+
vi.mock("@/lib/hooks/use-session-auth", () => ({
13+
useSessionAuth: mocks.useSessionAuth,
14+
}));
15+
16+
vi.mock("../../../../../lib/api-client", () => ({
17+
ApiError: class ApiError extends Error {
18+
constructor(
19+
public readonly status: number,
20+
public readonly code: string,
21+
message: string,
22+
public readonly body?: unknown,
23+
) {
24+
super(message);
25+
this.name = "ApiError";
26+
}
27+
},
28+
api: mocks.api,
29+
}));
30+
31+
vi.mock("./payment-navigation", () => ({
32+
navigateToExternalPayment: mocks.locationAssign,
33+
}));
34+
35+
import AppChargePaymentPage from "./page";
36+
37+
function appChargeDetails(status: "requested" | "confirmed" = "requested") {
38+
return {
39+
charge: {
40+
id: "charge_five",
41+
appId: "app_five",
42+
amountUsd: 5,
43+
description: "Please send me $5",
44+
providers: ["stripe", "oxapay"],
45+
paymentUrl: "http://localhost/payment/app-charge/app_five/charge_five",
46+
status,
47+
paidAt: status === "confirmed" ? new Date("2026-05-09T23:15:00Z").toISOString() : null,
48+
paidProvider: status === "confirmed" ? "oxapay" : undefined,
49+
providerPaymentId: status === "confirmed" ? "oxapay_payment_five" : undefined,
50+
expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
51+
createdAt: new Date("2026-05-09T23:00:00Z").toISOString(),
52+
},
53+
app: {
54+
id: "app_five",
55+
name: "Five Dollar Agent",
56+
description: "Agent payment request",
57+
logo_url: null,
58+
website_url: "https://example.com",
59+
},
60+
};
61+
}
62+
63+
function renderPaymentPage(initialPath = "/payment/app-charge/app_five/charge_five") {
64+
return render(
65+
<MemoryRouter initialEntries={[initialPath]}>
66+
<Routes>
67+
<Route path="/payment/app-charge/:appId/:chargeId" element={<AppChargePaymentPage />} />
68+
<Route path="/login" element={<div>login required</div>} />
69+
</Routes>
70+
</MemoryRouter>,
71+
);
72+
}
73+
74+
beforeEach(() => {
75+
mocks.api.mockReset();
76+
mocks.useSessionAuth.mockReset();
77+
mocks.locationAssign.mockReset();
78+
mocks.useSessionAuth.mockReturnValue({
79+
ready: true,
80+
authenticated: true,
81+
authSource: "steward",
82+
stewardAuthenticated: true,
83+
stewardUser: { id: "payer", email: "payer@example.com" },
84+
user: { id: "payer", email: "payer@example.com" },
85+
});
86+
});
87+
88+
afterEach(() => {
89+
cleanup();
90+
vi.useRealTimers();
91+
vi.restoreAllMocks();
92+
});
93+
94+
describe("AppChargePaymentPage", () => {
95+
test("renders the dynamic $5 charge and starts OxaPay checkout", async () => {
96+
mocks.api.mockImplementation(async (path: string, init?: { method?: string }) => {
97+
if (init?.method === "POST") {
98+
return {
99+
checkout: {
100+
provider: "oxapay",
101+
paymentId: "oxapay_payment_five",
102+
trackId: "track_five",
103+
payLink: "https://pay.oxapay.com/track_five",
104+
expiresAt: new Date(Date.now() + 900_000).toISOString(),
105+
},
106+
};
107+
}
108+
expect(path).toBe("/api/v1/apps/app_five/charges/charge_five");
109+
return appChargeDetails();
110+
});
111+
112+
renderPaymentPage();
113+
114+
expect(await screen.findByText("$5.00")).toBeInTheDocument();
115+
expect(screen.getByText("Five Dollar Agent")).toBeInTheDocument();
116+
117+
fireEvent.click(screen.getByRole("button", { name: /pay with crypto/i }));
118+
119+
await waitFor(() => {
120+
expect(mocks.api).toHaveBeenCalledWith(
121+
"/api/v1/apps/app_five/charges/charge_five/checkout",
122+
expect.objectContaining({
123+
method: "POST",
124+
json: expect.objectContaining({
125+
provider: "oxapay",
126+
return_url: expect.stringMatching(
127+
/\/payment\/success\?.*charge_request_id=charge_five.*app_id=app_five/,
128+
),
129+
}),
130+
}),
131+
);
132+
});
133+
expect(mocks.locationAssign).toHaveBeenCalledWith("https://pay.oxapay.com/track_five");
134+
});
135+
136+
test("redirects unauthenticated payers to login before checkout", async () => {
137+
mocks.useSessionAuth.mockReturnValue({
138+
ready: true,
139+
authenticated: false,
140+
authSource: "none",
141+
stewardAuthenticated: false,
142+
stewardUser: null,
143+
user: null,
144+
});
145+
mocks.api.mockResolvedValue(appChargeDetails());
146+
147+
renderPaymentPage();
148+
149+
expect(await screen.findByText("$5.00")).toBeInTheDocument();
150+
fireEvent.click(screen.getByRole("button", { name: /pay with card/i }));
151+
152+
expect(await screen.findByText("login required")).toBeInTheDocument();
153+
expect(mocks.api).toHaveBeenCalledTimes(1);
154+
});
155+
156+
test("polls after provider return until the charge is confirmed", async () => {
157+
mocks.api
158+
.mockResolvedValueOnce(appChargeDetails("requested"))
159+
.mockResolvedValueOnce(appChargeDetails("confirmed"));
160+
161+
renderPaymentPage("/payment/app-charge/app_five/charge_five?payment=success");
162+
163+
expect(await screen.findByText("$5.00")).toBeInTheDocument();
164+
expect(screen.getByText("Waiting for confirmation.")).toBeInTheDocument();
165+
166+
await waitFor(() => expect(mocks.api).toHaveBeenCalledTimes(2), { timeout: 4_500 });
167+
expect(await screen.findByText(/paid/i)).toBeInTheDocument();
168+
expect(screen.queryByText("Waiting for confirmation.")).not.toBeInTheDocument();
169+
}, 8_000);
170+
});

cloud/apps/frontend/src/pages/payment/app-charge/[appId]/[chargeId]/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
33
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
44
import { useSessionAuth } from "@/lib/hooks/use-session-auth";
55
import { ApiError, api } from "../../../../../lib/api-client";
6+
import { navigateToExternalPayment } from "./payment-navigation";
67

78
type AppChargeProvider = "stripe" | "oxapay";
89

@@ -191,7 +192,7 @@ export default function AppChargePaymentPage() {
191192
throw new Error("Payment provider did not return a checkout link.");
192193
}
193194

194-
window.location.assign(checkoutUrl);
195+
navigateToExternalPayment(checkoutUrl);
195196
} catch (checkoutError) {
196197
setError(normalizeError(checkoutError));
197198
setCheckoutProvider(null);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function navigateToExternalPayment(url: string): void {
2+
window.location.assign(url);
3+
}

cloud/packages/lib/services/app-charge-requests.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ export class AppChargeRequestsService {
336336
});
337337
const successUrl = new URL(redirects.successUrl);
338338
successUrl.searchParams.set("session_id", "{CHECKOUT_SESSION_ID}");
339+
successUrl.searchParams.set("app_id", params.appId);
339340
successUrl.searchParams.set("charge_request_id", request.id);
340341

341342
const checkoutMetadata = {
@@ -422,6 +423,7 @@ export class AppChargeRequestsService {
422423
defaultCancelUrl: request.paymentUrl,
423424
});
424425
const returnUrl = new URL(redirects.successUrl);
426+
returnUrl.searchParams.set("app_id", params.appId);
425427
returnUrl.searchParams.set("charge_request_id", request.id);
426428

427429
const payment = await cryptoPaymentsService.createPayment({

0 commit comments

Comments
 (0)