Skip to content

Commit 5834b66

Browse files
committed
✨ server: add panda signature
1 parent fd77e1b commit 5834b66

11 files changed

Lines changed: 1031 additions & 24 deletions

File tree

.changeset/quiet-tigers-sign.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@exactly/server": patch
3+
---
4+
5+
✨ add panda signature

.do/app.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ services:
118118
- key: PANDA_API_URL
119119
scope: RUN_TIME
120120
value: ${{ env.PANDA_API_URL }}
121+
- key: ISSUER_ADDRESS
122+
scope: RUN_TIME
123+
value: ${{ env.ISSUER_ADDRESS }}
121124
- key: PAX_API_KEY
122125
scope: RUN_TIME
123126
type: SECRET

server/api/card.ts

Lines changed: 204 additions & 17 deletions
Large diffs are not rendered by default.

server/hooks/panda.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,23 @@ import {
4747
} from "@exactly/common/generated/chain";
4848
import MIN_BORROW_INTERVAL from "@exactly/common/MIN_BORROW_INTERVAL";
4949
import revertReason from "@exactly/common/revertReason";
50-
import { Address, type Hash, type Hex } from "@exactly/common/validation";
50+
import { Address, Hex, type Hash } from "@exactly/common/validation";
5151
import { MATURITY_INTERVAL, splitInstallments } from "@exactly/lib";
5252

5353
import database, { cards, credentials, transactions } from "../database/index";
5454
import t, { f } from "../i18n";
5555
import keeper from "../utils/keeper";
5656
import { sendPushNotification } from "../utils/onesignal";
57-
import { collectors, createMutex, getMutex, getUser, headerValidator, signIssuerOp, updateUser } from "../utils/panda";
57+
import {
58+
collectors,
59+
createMutex,
60+
getMutex,
61+
getUser,
62+
headerValidator,
63+
signIssuerOp,
64+
updateUser,
65+
verifyPandaSignature,
66+
} from "../utils/panda";
5867
import publicClient from "../utils/publicClient";
5968
import revertFingerprint from "../utils/revertFingerprint";
6069
import risk, { feedback } from "../utils/sardine";
@@ -87,6 +96,8 @@ const BaseTransaction = v.object({
8796
authorizedAmount: v.nullish(v.number()),
8897
authorizationMethod: v.optional(v.string()),
8998
userId: v.string(),
99+
signature: v.optional(Hex),
100+
timestamp: v.optional(v.number()),
90101
}),
91102
});
92103

@@ -510,6 +521,32 @@ export default new Hono().post(
510521
Math.floor(new Date(payload.body.spend.authorizedAt).getTime() / 1000) -
511522
Number(BigInt(`0x${payload.id.replaceAll(/[^0-9a-f]/g, "")}`) % 3600n);
512523
const signature = await signIssuerOp({ account, amount: -refundAmount, timestamp }); // TODO replace with payload signature
524+
if (payload.body.spend.signature) {
525+
await startSpan(
526+
{
527+
name: "panda.signature",
528+
op: "panda.signature",
529+
attributes: {
530+
"signature.account": account,
531+
"signature.amount": String(-refundAmount),
532+
"signature.timestamp": String(payload.body.spend.timestamp ?? 0),
533+
},
534+
},
535+
(span) => {
536+
if (!payload.body.spend.signature) throw new Error("signature not found");
537+
if (!payload.body.spend.timestamp) throw new Error("timestamp not found");
538+
return verifyPandaSignature({
539+
account,
540+
amount: -refundAmount,
541+
timestamp: payload.body.spend.timestamp,
542+
signature: payload.body.spend.signature,
543+
}).then((valid) => {
544+
span.setAttribute("signature.valid", valid);
545+
if (!valid) captureException(new Error("invalid panda signature"), { level: "error" });
546+
});
547+
},
548+
).catch((error: unknown) => captureException(error, { level: "error" }));
549+
}
513550
try {
514551
await keeper.exaSend(
515552
{ name: "exa.refund", op: "exa.refund", attributes: { account } },
@@ -1097,6 +1134,33 @@ async function prepareCollection(
10971134
(payload.body.spend.authorizedAt ? new Date(payload.body.spend.authorizedAt) : new Date()).getTime() / 1000, // TODO remove fallback
10981135
);
10991136
const signature = await signIssuerOp({ account, amount, timestamp }); // TODO replace with payload signature
1137+
if (payload.body.spend.signature) {
1138+
await startSpan(
1139+
{
1140+
name: "panda.signature",
1141+
op: "panda.signature",
1142+
attributes: {
1143+
"signature.account": account,
1144+
"signature.amount": String(amount),
1145+
"signature.timestamp": String(payload.body.spend.timestamp ?? 0),
1146+
},
1147+
},
1148+
(span) => {
1149+
if (!payload.body.spend.signature) throw new Error("signature not found");
1150+
if (!payload.body.spend.timestamp) throw new Error("timestamp not found");
1151+
return verifyPandaSignature({
1152+
account,
1153+
amount,
1154+
timestamp: payload.body.spend.timestamp,
1155+
signature: payload.body.spend.signature,
1156+
}).then((valid) => {
1157+
span.setAttribute("signature.valid", valid);
1158+
if (!valid) captureException(new Error("invalid panda signature"), { level: "error" });
1159+
});
1160+
},
1161+
).catch((error: unknown) => captureException(error, { level: "error" }));
1162+
}
1163+
11001164
if (card.mode === 0) {
11011165
return { functionName: "collectDebit", args: [amount, BigInt(timestamp), signature] } as const;
11021166
}

server/script/openapi.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { generateSpecs } from "hono-openapi";
22
import { writeFile } from "node:fs/promises";
33
import { padHex } from "viem";
4+
import { privateKeyToAddress } from "viem/accounts";
45

56
import { version } from "../package.json";
67

@@ -18,6 +19,7 @@ process.env.MANTECA_API_URL = "https://manteca.test";
1819
process.env.MANTECA_WEBHOOKS_KEY = "manteca";
1920
process.env.PANDA_API_KEY = "panda";
2021
process.env.PANDA_API_URL = "https://panda.test";
22+
process.env.ISSUER_ADDRESS = privateKeyToAddress(padHex("0x420"));
2123
process.env.PAX_API_KEY = "pax";
2224
process.env.PAX_API_URL = "https://pax.test";
2325
process.env.PAX_ASSOCIATE_ID_KEY = "pax";

0 commit comments

Comments
 (0)