Skip to content

Conversation

@d4mr
Copy link
Contributor

@d4mr d4mr commented Feb 10, 2025

PR-Codex overview

This PR introduces a WalletSubscriptions feature, allowing users to create, update, retrieve, and delete wallet subscriptions based on specified conditions. It also integrates a worker to process these subscriptions and trigger webhooks when conditions are met.

Detailed summary

  • Added WalletSubscriptionsService for managing wallet subscriptions.
  • Introduced CRUD routes for wallet subscriptions: add, update, get-all, and delete.
  • Implemented a worker (WalletSubscriptionWorker) to process subscriptions and trigger webhooks.
  • Created database schema and migration for wallet_subscriptions.
  • Updated webhook event types to include WALLET_SUBSCRIPTION.
  • Added tests for wallet subscription functionality.
  • Enhanced error handling and validation for subscriptions and webhooks.

✨ Ask PR-Codex anything about this PR by commenting with /codex {your question}

@zeet-co
Copy link

zeet-co bot commented Feb 10, 2025

We're building your pull request over on Zeet.
Click me for more info about your build and deployment.
Once built, this branch can be tested at: https://tw-unreal-demo-engine-vwn0-pb-bal-fbef39.engine-aws-usw2.zeet.app before merging 😉

client: thirdwebClient,
});

currentBalance = await balanceOf({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait what is contract here? this is to get a erc20 balance, that's not what you want here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you can subscribe to either erc20 balance or native balance changes

- Add support for creating new webhooks with optional labels
- Allow using existing webhooks by ID
- Validate webhook event type and revocation status
- Improve error handling for webhook creation and selection
- Replace manual balance fetching with `getWalletBalance` method
- Support both native token and ERC20 token balance retrieval
- Remove redundant contract and RPC client initialization code
…ions

- Update Prisma schema, migration, and database indexes
- Modify TypeScript interfaces and schemas across services and routes
- Ensure consistent naming for token-related address fields
- Update worker and database interaction methods
description: "Webhook URL to create a new webhook",
examples: ["https://example.com/webhook"],
}),
webhookLabel: Type.Optional(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can make this non-optional

threshold: Type.Optional(
Type.Object({
min: Type.Optional(Type.String({
description: "Minimum balance threshold",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want the dev to provide wei, we should explicitly make that clear and remind them the token's decimals matter. E.g. 0.1 ETH and 0.1 USDC will have different values.

I'm debating if tokens ("0.1") is more intuitive and less error prone (to get decimals wrong). Also precision doesn't matter because it's just a threshold anyway.

cursorDelaySeconds Int @default(2) @map("cursorDelaySeconds")
contractSubscriptionsRetryDelaySeconds String @default("10") @map("contractSubscriptionsRetryDelaySeconds")
balanceSubscriptionsCronSchedule String? @map("balanceSubscriptionsCronSchedule")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expose this in some config update route?

@d4mr d4mr requested a review from arcoraven February 13, 2025 00:44
/**
* Array of conditions to monitor for this wallet
*/
conditions: Array<({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a reusable type for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's autogen 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh lol mb

validateConditions,
} from "../../shared/schemas/wallet-subscription-conditions";

interface WalletSubscriptionWithWebhook {
Copy link
Contributor

@arcoraven arcoraven Feb 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need to define a different interface right? Shouldn't we be passing the type
import type { WalletSubscription } from "@prisma/client" around?

Or if we intend to pass the type joined with Webhook, then something like:

type WalletSubscriptionWithWebhook =
  WalletSubscriptions & {
    webhook: Webhooks
  }

import type { WalletCondition } from "../../shared/schemas/wallet-subscription-conditions";
import type { Webhooks } from "@prisma/client";

interface WalletSubscriptionWithWebhook {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment, we shouldn't need to define a new interface

chain,
});

if (currentValue && subscription.webhookId && subscription.webhook) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't webhook always be set already?
We should assert(subscription.webhook) above then and not do any work if there's no valid webhook to even receive the request (e.g. deleted webhook).

}
} catch (error) {
// Log error but continue processing other subscriptions
const message = error instanceof Error ? error.message : String(error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use prettifyError(error)

Comment on lines 155 to 158
const subscriptions = await getAllWalletSubscriptions({
page: 1,
limit: 1000, // Process 1000 subscriptions at a time
});
Copy link
Contributor

@arcoraven arcoraven Feb 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't put this limit here because Engine allows creating > 1k subscriptions, and any more are silently ignored.

Instead just enforce this limit on the "create wallet subscription" endpoint: If > 1k non-deleted subscriptions, don't allow creating any more.

Comment on lines 72 to 76
default: {
// For TypeScript exhaustiveness check
const _exhaustiveCheck: never = condition;
return undefined;
}
Copy link
Contributor

@arcoraven arcoraven Feb 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need this if you type condition.type properly. Typescript knows theres no unhandled cases.

I see WalletCondition already should have type: 'token_balance_lt' | 'token_balance_gt' because it infers from a Zod schema that enforces this. Does typescript error if you remove this default case?

condition: WalletCondition;
walletAddress: string;
chain: Chain;
}): Promise<string | undefined> {
Copy link
Contributor

@arcoraven arcoraven Feb 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: null seems more correct than undefined here, as an explicit "I checked and there is no value to return".

@d4mr d4mr merged commit 503f37e into main Feb 18, 2025
7 checks passed
@d4mr d4mr deleted the pb/balance-subscriptions branch February 18, 2025 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants