Skip to content

Commit a999de6

Browse files
authored
Merge pull request #73 from Dnreikronos/fix/verify-delegation-instruction
fix: verify approve_delegate discriminator and PDA in delegation check
2 parents 1b604ff + b9dedd2 commit a999de6

8 files changed

Lines changed: 44 additions & 5 deletions

File tree

backend/src/controllers/subscribe.controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export const subscribe = async (
113113
const verification = await verifyDelegationTransaction({
114114
txSignature: validatedData.delegateTxSignature,
115115
expectedPayerWallet: validatedData.payer.walletAddress,
116+
subscriptionId: validatedData.subscriptionId,
116117
});
117118

118119
if (!verification.valid) {

backend/src/controllers/subscription.controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const createSubscription = async (
7272
const verification = await verifyDelegationTransaction({
7373
txSignature: validatedData.delegateTxSignature,
7474
expectedPayerWallet: payer.walletAddress,
75+
subscriptionId: validatedData.subscriptionId,
7576
});
7677

7778
if (!verification.valid) {

backend/src/routes/subscribe.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export async function subscribeRoutes(fastify: FastifyInstance) {
2727
required: [
2828
"payer",
2929
"planId",
30+
"subscriptionId",
3031
"tokenMint",
3132
"delegateTxSignature",
3233
"delegateAuthority",
@@ -62,6 +63,12 @@ export async function subscribeRoutes(fastify: FastifyInstance) {
6263
format: "uuid",
6364
description: "ID of the recurring payment plan to subscribe to",
6465
},
66+
subscriptionId: {
67+
type: "string",
68+
format: "uuid",
69+
description:
70+
"UUID generated client-side and used as the on-chain PDA seed for approve_delegate",
71+
},
6572
tokenMint: {
6673
type: "string",
6774
description:

backend/src/schemas/subscribe.schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const subscribeSchema = z.object({
2121
}),
2222

2323
planId: z.uuid("Invalid plan ID"),
24+
subscriptionId: z.uuid("Invalid subscription ID"),
2425
tokenMint: z
2526
.string()
2627
.regex(

backend/src/schemas/subscription.schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import z from "zod";
2727
export const createSubscriptionSchema = z.object({
2828
planId: z.string().uuid("Invalid plan ID"),
2929
payerId: z.string().uuid("Invalid payer ID"),
30+
subscriptionId: z.string().uuid("Invalid subscription ID"),
3031
tokenMint: z
3132
.string()
3233
.regex(

backend/src/services/solana.service.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,14 @@ export const verifyOneTimePayment = async (params: {
225225
};
226226
};
227227

228+
const APPROVE_DELEGATE_DISCRIMINATOR = Buffer.from([
229+
68, 6, 248, 64, 195, 222, 182, 223,
230+
]);
231+
228232
export const verifyDelegationTransaction = async (params: {
229233
txSignature: string;
230234
expectedPayerWallet: string;
235+
subscriptionId: string;
231236
}): Promise<{ valid: boolean; reason?: string }> => {
232237
const { connection } = initializeSolana();
233238

@@ -242,14 +247,20 @@ export const verifyDelegationTransaction = async (params: {
242247
const programIdStr = PROGRAM_ID.toBase58();
243248
const instructions = tx.transaction.message.instructions;
244249

245-
const programInvoked = instructions.some(
246-
(ix) => ix.programId.toBase58() === programIdStr,
247-
);
250+
const approveDelegateInvoked = instructions.some((ix) => {
251+
if (ix.programId.toBase58() !== programIdStr) return false;
252+
if (!("data" in ix)) return false;
253+
const decoded = Buffer.from(anchor.utils.bytes.bs58.decode(ix.data as string));
254+
return decoded
255+
.subarray(0, 8)
256+
.equals(APPROVE_DELEGATE_DISCRIMINATOR);
257+
});
248258

249-
if (!programInvoked) {
259+
if (!approveDelegateInvoked) {
250260
return {
251261
valid: false,
252-
reason: "Transaction did not invoke the PattPay program",
262+
reason:
263+
"Transaction did not invoke the approve_delegate instruction on the PattPay program",
253264
};
254265
}
255266

@@ -264,6 +275,21 @@ export const verifyDelegationTransaction = async (params: {
264275
};
265276
}
266277

278+
const payerPubkey = new PublicKey(params.expectedPayerWallet);
279+
const { delegateApprovalPDA } = derivePDAs(
280+
params.subscriptionId,
281+
payerPubkey,
282+
);
283+
const accountInfo = await connection.getAccountInfo(delegateApprovalPDA);
284+
285+
if (accountInfo === null) {
286+
return {
287+
valid: false,
288+
reason:
289+
"DelegateApproval PDA does not exist on-chain, delegation not confirmed",
290+
};
291+
}
292+
267293
return { valid: true };
268294
};
269295

frontend/lib/hooks/usePaymentMutations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export function useSubscribe() {
6565
email: data.email,
6666
},
6767
planId: data.planId,
68+
subscriptionId: data.subscriptionId,
6869
tokenMint: data.tokenMint,
6970
delegateTxSignature: approvalResult.txSignature,
7071
delegateAuthority: approvalResult.delegateAuthority,

frontend/lib/types/subscription.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface SubscribeRequest {
1212

1313
// Subscription details
1414
planId: string;
15+
subscriptionId: string;
1516
tokenMint: string;
1617

1718
// On-chain delegation proof

0 commit comments

Comments
 (0)