Skip to content

Commit fa5e13b

Browse files
committed
Feature: Adds onramp webhook schema to sdk (#7450)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on adding onramp webhook parsing functionality to the `Universal Bridge` in the `thirdweb` package, along with updates to existing tests to accommodate new payload structures. ### Detailed summary - Added onramp webhook parsing for `Universal Bridge`. - Updated `chainId` and `transactionHash` values in tests. - Modified the structure of `validWebhook` and `validPayload` in `Webhook.test.ts`. - Changed `destinationAmount` and `originAmount` types to `bigint`. - Introduced `onchainWebhookSchema` and `onrampWebhookSchema` in `Webhook.ts`. - Updated `webhookSchema` to include `onchain` and `onramp` transaction types. - Enhanced the `parse` function to return a `WebhookPayload` that satisfies the new schema. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enhanced webhook parsing to support onramp webhooks for the Universal Bridge. * **Refactor** * Improved validation and structure of webhook payloads for clearer distinction between onchain and onramp types. * Updated numeric fields to ensure correct data types and improved payload handling. * **Documentation** * Added a changeset file describing the new onramp webhook parsing capability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 45bb790 commit fa5e13b

File tree

4 files changed

+74
-33
lines changed

4 files changed

+74
-33
lines changed

.changeset/eleven-taxis-mix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Adds onramp webhook parsing for Universal Bridge

packages/thirdweb/src/bridge/Status.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ describe.runIf(process.env.TW_SECRET_KEY)("Bridge.status", () => {
77
// TODO: flaky test
88
it.skip("should handle successful status", async () => {
99
const result = await status({
10-
chainId: 137,
10+
chainId: 8453,
1111
client: TEST_CLIENT,
1212
transactionHash:
13-
"0x5959b9321ec581640db531b80bac53cbd968f3d34fc6cb1d5f4ea75f26df2ad7",
13+
"0x8e8ab7c998bdfef6e10951c801a862373ce87af62c21fb870e62fca57683bf10",
1414
});
1515

1616
expect(result).toBeDefined();
@@ -46,7 +46,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("Bridge.status", () => {
4646
chain: defineChain(8453),
4747
client: TEST_CLIENT,
4848
transactionHash:
49-
"0x06ac91479b3ea4c6507f9b7bff1f2d5f553253fa79af9a7db3755563b60f7dfb",
49+
"0x8e8ab7c998bdfef6e10951c801a862373ce87af62c21fb870e62fca57683bf10",
5050
});
5151

5252
expect(result).toBeDefined();

packages/thirdweb/src/bridge/Webhook.test.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ const generateSignature = async (
2121

2222
describe("parseIncomingWebhook", () => {
2323
const testTimestamp = Math.floor(Date.now() / 1000).toString();
24-
const validPayload: WebhookPayload = {
24+
const validWebhook: WebhookPayload = {
2525
data: {
2626
action: "TRANSFER",
2727
clientId: "client123",
28-
destinationAmount: "1.0",
28+
destinationAmount: 10n,
2929
destinationToken: {
3030
address: "0x1234567890123456789012345678901234567890" as const,
3131
chainId: 1,
@@ -37,7 +37,7 @@ describe("parseIncomingWebhook", () => {
3737
},
3838
developerFeeBps: 100,
3939
developerFeeRecipient: "0x1234567890123456789012345678901234567890",
40-
originAmount: "1.0",
40+
originAmount: 10n,
4141
originToken: {
4242
address: "0x1234567890123456789012345678901234567890" as const,
4343
chainId: 1,
@@ -64,8 +64,17 @@ describe("parseIncomingWebhook", () => {
6464
],
6565
type: "transfer",
6666
},
67+
type: "pay.onchain-transaction",
6768
version: 2,
6869
};
70+
const validPayload = {
71+
...validWebhook,
72+
data: {
73+
...validWebhook.data,
74+
destinationAmount: validWebhook.data.destinationAmount.toString(),
75+
originAmount: validWebhook.data.originAmount.toString(),
76+
},
77+
};
6978

7079
it("should successfully verify a valid webhook", async () => {
7180
const signature = await generateSignature(
@@ -78,7 +87,7 @@ describe("parseIncomingWebhook", () => {
7887
};
7988

8089
const result = await parse(JSON.stringify(validPayload), headers, secret);
81-
expect(result).toEqual(validPayload);
90+
expect(result).toEqual(validWebhook);
8291
});
8392

8493
it("should accept alternative header names", async () => {
@@ -92,7 +101,7 @@ describe("parseIncomingWebhook", () => {
92101
};
93102

94103
const result = await parse(JSON.stringify(validPayload), headers, secret);
95-
expect(result).toEqual(validPayload);
104+
expect(result).toEqual(validWebhook);
96105
});
97106

98107
it("should throw error for missing headers", async () => {
@@ -149,6 +158,7 @@ describe("parseIncomingWebhook", () => {
149158
data: {
150159
someField: "value",
151160
},
161+
type: "pay.onchain-transaction",
152162
version: 1,
153163
};
154164
const v1PayloadString = JSON.stringify(v1Payload);
@@ -180,7 +190,7 @@ describe("parseIncomingWebhook", () => {
180190
secret,
181191
300,
182192
);
183-
expect(result).toEqual(validPayload);
193+
expect(result).toEqual(validWebhook);
184194
});
185195

186196
describe("payload validation", () => {
@@ -575,6 +585,7 @@ describe("parseIncomingWebhook", () => {
575585

576586
it("should throw error for version 1 payload missing data object", async () => {
577587
const invalidPayload = {
588+
type: "pay.onchain-transaction",
578589
version: 1,
579590
// no data field
580591
} as unknown as WebhookPayload;

packages/thirdweb/src/bridge/Webhook.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,32 @@ const hexSchema = z
88
const addressSchema = z
99
.string()
1010
.check(z.refine(isAddress, { message: "Invalid address" }));
11-
12-
const webhookSchema = z.union([
11+
const tokenSchema = z.object({
12+
address: addressSchema,
13+
chainId: z.coerce.number(),
14+
decimals: z.coerce.number(),
15+
iconUri: z.optional(z.string()),
16+
name: z.string(),
17+
priceUsd: z.coerce.number(),
18+
symbol: z.string(),
19+
});
20+
21+
const onchainWebhookSchema = z.discriminatedUnion("version", [
1322
z.object({
1423
data: z.object({}),
24+
type: z.literal("pay.onchain-transaction"),
1525
version: z.literal(1),
1626
}),
1727
z.object({
1828
data: z.object({
1929
action: z.enum(["TRANSFER", "BUY", "SELL"]),
2030
clientId: z.string(),
21-
destinationAmount: z.string(),
22-
destinationToken: z.object({
23-
address: addressSchema,
24-
chainId: z.coerce.number(),
25-
decimals: z.coerce.number(),
26-
iconUri: z.optional(z.string()),
27-
name: z.string(),
28-
priceUsd: z.coerce.number(),
29-
symbol: z.string(),
30-
}),
31+
destinationAmount: z.coerce.bigint(),
32+
destinationToken: tokenSchema,
3133
developerFeeBps: z.coerce.number(),
3234
developerFeeRecipient: addressSchema,
33-
originAmount: z.string(),
34-
originToken: z.object({
35-
address: addressSchema,
36-
chainId: z.coerce.number(),
37-
decimals: z.coerce.number(),
38-
iconUri: z.optional(z.string()),
39-
name: z.string(),
40-
priceUsd: z.coerce.number(),
41-
symbol: z.string(),
42-
}),
35+
originAmount: z.coerce.bigint(),
36+
originToken: tokenSchema,
4337
paymentId: z.string(),
4438
// only exists when the payment was triggered from a developer specified payment link
4539
paymentLinkId: z.optional(z.string()),
@@ -55,10 +49,41 @@ const webhookSchema = z.union([
5549
),
5650
type: z.string(),
5751
}),
52+
type: z.literal("pay.onchain-transaction"),
53+
version: z.literal(2),
54+
}),
55+
]);
56+
57+
const onrampWebhookSchema = z.discriminatedUnion("version", [
58+
z.object({
59+
data: z.object({}),
60+
type: z.literal("pay.onramp-transaction"),
61+
version: z.literal(1),
62+
}),
63+
z.object({
64+
data: z.object({
65+
amount: z.coerce.bigint(),
66+
currency: z.string(),
67+
currencyAmount: z.number(),
68+
id: z.string(),
69+
onramp: z.string(),
70+
paymentLinkId: z.optional(z.string()),
71+
purchaseData: z.unknown(),
72+
receiver: z.optional(addressSchema),
73+
sender: z.optional(addressSchema),
74+
status: z.enum(["PENDING", "COMPLETED", "FAILED"]),
75+
token: tokenSchema,
76+
transactionHash: z.optional(hexSchema),
77+
}),
78+
type: z.literal("pay.onramp-transaction"),
5879
version: z.literal(2),
5980
}),
6081
]);
6182

83+
const webhookSchema = z.discriminatedUnion("type", [
84+
onchainWebhookSchema,
85+
onrampWebhookSchema,
86+
]);
6287
export type WebhookPayload = Exclude<
6388
z.infer<typeof webhookSchema>,
6489
{ version: 1 }
@@ -93,7 +118,7 @@ export async function parse(
93118
* The tolerance in seconds for the timestamp verification.
94119
*/
95120
tolerance = 300, // Default to 5 minutes if not specified
96-
) {
121+
): Promise<WebhookPayload> {
97122
// Get the signature and timestamp from headers
98123
const receivedSignature =
99124
headers["x-payload-signature"] || headers["x-pay-signature"];
@@ -158,5 +183,5 @@ export async function parse(
158183
);
159184
}
160185

161-
return parsedPayload;
186+
return parsedPayload satisfies WebhookPayload;
162187
}

0 commit comments

Comments
 (0)