Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for backend transactions sent as backend wallet #200

Merged
merged 14 commits into from
Jan 7, 2025
Merged
5 changes: 5 additions & 0 deletions .changeset/serious-dancers-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@treasure-dev/tdk-react": patch
---

Ensure initial user login occurs on provided default chain
5 changes: 5 additions & 0 deletions .changeset/thin-buses-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@treasure-dev/tdk-core": minor
---

Added utilities for generating and verifying backend wallet signatures
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ SDK for the Treasure ecosystem
- [Treasure Connect (React)](./examples/connect-react) - [Live Demo](https://tdk-react-demo.spellcaster.lol)
- [Treasure Connect (Electron)](./examples/connect-electron)
- [Magicswap](./examples/magicswap) - [Live Demo](https://tdk-magicswap-demo.spellcaster.lol)
- [Backend-to-Backend](./examples/backend-to-backend)

## Development

Expand Down
131 changes: 89 additions & 42 deletions apps/api/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,123 @@
import * as Sentry from "@sentry/node";
import {
type AddressString,
type UserContext,
verifyAccountSignature,
verifyBackendWalletSignature,
} from "@treasure-dev/tdk-core";
import { type Address, isAddress, isHex } from "thirdweb";

import { type Hex, isHex } from "thirdweb";
import type { TdkApiContext } from "../types";
import type { App } from "../utils/app";
import { throwUnauthorizedError } from "../utils/error";
import { createUnauthorizedError } from "../utils/error";

declare module "fastify" {
interface FastifyRequest {
userId: string | undefined;
userAddress: AddressString | undefined;
backendUserAddress: AddressString | undefined;
backendWallet: AddressString | undefined;
authError: string | undefined;
isAuthenticated: boolean;
authenticatedUserId: string | undefined;
authenticatedUserAddress: Address | undefined;
authenticationError: string | undefined;
isBackendRequest: boolean;
verifiedBackendWallet: Address | undefined;
verifiedBackendUserAddress: Address | undefined;
}
}

export const withAuth = (app: App, { auth }: TdkApiContext) => {
app.decorateRequest("userId", undefined);
app.decorateRequest("userAddress", undefined);
app.decorateRequest("backendUserAddress", undefined);
app.decorateRequest("backendWallet", undefined);
app.decorateRequest("authError", undefined);
export const withAuth = (app: App, { auth, client }: TdkApiContext) => {
app.decorateRequest("isAuthenticated", false);
app.decorateRequest("authenticatedUserId", undefined);
app.decorateRequest("authenticatedUserAddress", undefined);
app.decorateRequest("authenticationError", undefined);
app.decorateRequest("isBackendRequest", false);
app.decorateRequest("verifiedBackendWallet", undefined);
app.decorateRequest("verifiedBackendUserAddress", undefined);
app.addHook("onRequest", async (req) => {
// Check for explicit setting of user address and recover backend wallet address from signature
if (req.headers["x-account-address"]) {
const accountAddress = req.headers["x-account-address"].toString();
const signature = req.headers["x-account-signature"]?.toString();
if (!isHex(accountAddress) || !isHex(signature)) {
throwUnauthorizedError("Invalid account address or signature");
if (!isAddress(accountAddress)) {
throw createUnauthorizedError("Invalid account address");
}

const accountSignature = req.headers["x-account-signature"]?.toString();
if (!isHex(accountSignature)) {
throw createUnauthorizedError("Invalid account signature");
}

const expirationTime = Number(
req.headers["x-account-signature-expiration"],
);

const backendWallet = await verifyAccountSignature({
accountAddress,
signature: signature as Hex,
signature: accountSignature,
expirationTime,
});
if (!backendWallet) {
throwUnauthorizedError("Invalid backend wallet address");
}

req.backendUserAddress = accountAddress as AddressString;
req.backendWallet = backendWallet as AddressString;
Sentry.setUser({ username: req.backendUserAddress });
req.isBackendRequest = true;
req.verifiedBackendWallet = backendWallet as Address;
req.verifiedBackendUserAddress = accountAddress;
Sentry.setUser({ username: req.verifiedBackendUserAddress });
return;
}

// All other auth methods require an Authorization header
if (!req.headers.authorization) {
// Check for explicit setting of backend wallet and recover address from signature
if (req.headers["x-backend-wallet-signature"]) {
const backendWallet = req.headers["x-backend-wallet"]?.toString();
if (!backendWallet || !isAddress(backendWallet)) {
throw createUnauthorizedError("Invalid backend wallet address");
}

const backendWalletSignature =
req.headers["x-backend-wallet-signature"]?.toString();
if (!isHex(backendWalletSignature)) {
throw createUnauthorizedError("Invalid backend wallet signature");
}

const expirationTime = Number(
req.headers["x-backend-wallet-signature-expiration"],
);
if (Number.isNaN(expirationTime)) {
throw createUnauthorizedError(
"Invalid backend wallet signature expiration",
);
}

const verifiedBackendWallet = await verifyBackendWalletSignature({
client,
chainId: req.chain.id,
backendWallet,
signature: backendWalletSignature,
expirationTime,
});

req.isBackendRequest = true;
req.verifiedBackendWallet =
verifiedBackendWallet.toLowerCase() as Address;
return;
}

// Check for user address via JWT header
try {
const decoded = await auth.verifyJWT<UserContext>(
req.headers.authorization.replace("Bearer ", ""),
);
req.userId = decoded.ctx.id;
req.userAddress = (decoded.ctx.smartAccounts.find(
({ chainId }) => chainId === req.chain.id,
)?.address ??
decoded.ctx.address ??
decoded.sub) as AddressString;
Sentry.setUser({
id: req.userId,
username: req.userAddress,
});
} catch (err) {
req.authError = err instanceof Error ? err.message : "Unknown error";
if (req.headers.authorization) {
try {
const decoded = await auth.verifyJWT<UserContext>(
req.headers.authorization.replace("Bearer ", ""),
);
const smartAccount = decoded.ctx.smartAccounts.find(
(account) => account.chainId === req.chain.id,
);
req.isAuthenticated = true;
req.authenticatedUserId = decoded.ctx.id;
req.authenticatedUserAddress = (smartAccount?.address ??
decoded.ctx.address ??
decoded.sub) as Address;
Sentry.setUser({
id: req.authenticatedUserId,
username: req.authenticatedUserAddress,
});
} catch (err) {
req.authenticationError =
err instanceof Error ? err.message : "Unknown error";
}
}
});
};
66 changes: 44 additions & 22 deletions apps/api/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import {
USER_SMART_ACCOUNT_SELECT_FIELDS,
USER_SOCIAL_ACCOUNT_SELECT_FIELDS,
} from "../utils/db";
import { throwUnauthorizedError, throwUserNotFoundError } from "../utils/error";
import {
createUnauthorizedError,
createUserNotFoundError,
} from "../utils/error";
import { log } from "../utils/log";
import {
migrateLegacyUser,
Expand Down Expand Up @@ -99,22 +102,34 @@ export const authRoutes =
body: {
payload: unverifiedPayload,
signature,
adminAccount: adminAccountData,
authTokenDurationSec = 86_400, // 1 day
},
} = req;
const verifiedPayload = await thirdwebAuth.verifyPayload({
payload: unverifiedPayload,
signature,
});
if (!verifiedPayload.valid) {
const [verifyPayloadResult, verifyAdminAccountPayloadResult] =
await Promise.all([
thirdwebAuth.verifyPayload({
payload: unverifiedPayload,
signature,
}),
adminAccountData
? thirdwebAuth.verifyPayload({
payload: adminAccountData.payload,
signature: adminAccountData.signature,
})
: undefined,
]);
if (!verifyPayloadResult.valid) {
return reply
.code(400)
.send({ error: `Login failed: ${verifiedPayload.error}` });
.send({ error: `Login failed: ${verifyPayloadResult.error}` });
}

const { payload } = verifiedPayload;
const chainId = Number(payload.chain_id ?? DEFAULT_TDK_CHAIN_ID);
const address = payload.address.toLowerCase();
const verifiedPayload = verifyPayloadResult.payload;
const chainId = Number(
verifiedPayload.chain_id ?? DEFAULT_TDK_CHAIN_ID,
);
const address = verifiedPayload.address.toLowerCase();
const chain = defineChain(chainId);

const foundUserSmartAccount = await db.userSmartAccount.findUnique({
Expand Down Expand Up @@ -149,19 +164,27 @@ export const authRoutes =
// On ZKsync chains, the smart account address is the same as the admin wallet address
if (await isZkSyncChain(chain)) {
adminWalletAddress = address.toLowerCase();
} else if (verifyAdminAccountPayloadResult?.valid) {
adminWalletAddress =
verifyAdminAccountPayloadResult.payload.address;
} else {
// Fetch admin wallets associated with this smart account address
const { result } = await engine.account.getAllAdmins(
chainId.toString(),
address,
);
adminWalletAddress = result[0]?.toLowerCase();
try {
// Fetch admin wallets associated with this smart account address
const { result } = await engine.account.getAllAdmins(
chainId.toString(),
address,
);
adminWalletAddress = result[0]?.toLowerCase();
} catch (err) {
log.error(`Error fetching admin wallets: ${err}`);
}
}

// Smart accounts should never be orphaned, but checking anyway
if (!adminWalletAddress) {
throwUnauthorizedError("No admin wallet found for smart account");
return;
throw createUnauthorizedError(
"No admin wallet found for smart account",
);
}

// Fetch Thirdweb user details by ecosystem wallet address
Expand Down Expand Up @@ -258,13 +281,12 @@ export const authRoutes =
]);

if (!user) {
throwUserNotFoundError();
return;
throw createUserNotFoundError();
}

const [authTokenResult, userSessionsResult] = await Promise.allSettled([
auth.generateJWT<UserContext>(address, {
issuer: payload.domain,
issuer: verifiedPayload.domain,
issuedAt: new Date(),
expiresAt: new Date(
new Date().getTime() + authTokenDurationSec * 1000,
Expand Down Expand Up @@ -370,7 +392,7 @@ export const authRoutes =
return reply.send(user);
}

throwUnauthorizedError("Invalid request");
throw createUnauthorizedError("Invalid request");
},
);
};
6 changes: 3 additions & 3 deletions apps/api/src/routes/harvesters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const harvestersRoutes =
const {
chain,
params: { id },
userAddress,
authenticatedUserAddress,
} = req;

const harvesterAddress = id as AddressString;
Expand All @@ -62,11 +62,11 @@ export const harvestersRoutes =
});
}

const harvesterUserInfo = userAddress
const harvesterUserInfo = authenticatedUserAddress
? await getHarvesterUserInfo({
chainId: chain.id,
harvesterInfo,
userAddress,
userAddress: authenticatedUserAddress,
inventoryApiUrl: env.TROVE_API_URL,
inventoryApiKey: env.TROVE_API_KEY,
wagmiConfig,
Expand Down
Loading
Loading