Skip to content

Commit 50a87c6

Browse files
add-enclave-functionality
1 parent 3496ef4 commit 50a87c6

File tree

11 files changed

+225
-44
lines changed

11 files changed

+225
-44
lines changed

src/server/routes/contract/write/write.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import {
1616
import { txOverridesWithValueSchema } from "../../../schemas/tx-overrides";
1717
import {
1818
maybeAddress,
19-
requiredAddress,
20-
walletWithAAHeaderSchema,
19+
type walletWithAAHeaderSchema, walletWithAAOrEnclaveHeaderSchema,
2120
} from "../../../schemas/wallet";
2221
import { sanitizeAbi, sanitizeFunctionName } from "../../../utils/abi";
2322
import { getChainIdFromChain } from "../../../utils/chain";
2423
import { parseTransactionOverrides } from "../../../utils/transaction-overrides";
24+
import { parseEnclaveHeaders } from "../../../utils/convertor";
2525

2626
// INPUT
2727
const writeRequestBodySchema = Type.Object({
@@ -60,7 +60,7 @@ export async function writeToContract(fastify: FastifyInstance) {
6060
tags: ["Contract"],
6161
operationId: "write",
6262
params: contractParamSchema,
63-
headers: walletWithAAHeaderSchema,
63+
headers: walletWithAAOrEnclaveHeaderSchema,
6464
querystring: requestQuerystringSchema,
6565
body: writeRequestBodySchema,
6666
response: {
@@ -72,12 +72,13 @@ export async function writeToContract(fastify: FastifyInstance) {
7272
const { simulateTx } = request.query;
7373
const { functionName, args, txOverrides, abi } = request.body;
7474
const {
75-
"x-backend-wallet-address": fromAddress,
75+
"x-backend-wallet-address": backendWalletAddress,
7676
"x-account-address": accountAddress,
7777
"x-idempotency-key": idempotencyKey,
7878
"x-account-factory-address": accountFactoryAddress,
7979
"x-account-salt": accountSalt,
8080
} = request.headers as Static<typeof walletWithAAHeaderSchema>;
81+
const enclave = parseEnclaveHeaders(request.headers);
8182

8283
const chainId = await getChainIdFromChain(chain);
8384
const contract = await getContractV5({
@@ -118,14 +119,15 @@ export async function writeToContract(fastify: FastifyInstance) {
118119
const queueId = await queueTransaction({
119120
functionName,
120121
transaction,
121-
fromAddress: requiredAddress(fromAddress, "x-backend-wallet-address"),
122+
fromAddress: maybeAddress(backendWalletAddress, "x-backend-wallet-address"),
122123
toAddress: maybeAddress(contractAddress, "to"),
123124
accountAddress: maybeAddress(accountAddress, "x-account-address"),
124125
accountFactoryAddress: maybeAddress(
125126
accountFactoryAddress,
126127
"x-account-factory-address",
127128
),
128129
accountSalt,
130+
enclave,
129131
txOverrides,
130132
idempotencyKey,
131133
shouldSimulate: simulateTx,

src/server/schemas/wallet/index.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,48 @@ import { badAddressError } from "../../middleware/error";
55
import { AddressSchema } from "../address";
66
import { chainIdOrSlugSchema } from "../chain";
77

8-
export const walletHeaderSchema = Type.Object({
9-
"x-backend-wallet-address": {
10-
...AddressSchema,
11-
description: "Backend wallet address",
12-
},
8+
export const idempotentHeaderSchema = Type.Object({
139
"x-idempotency-key": Type.Optional(
1410
Type.String({
1511
maxLength: 200,
1612
description: `Transactions submitted with the same idempotency key will be de-duplicated. Only the last ${env.TRANSACTION_HISTORY_COUNT} transactions are compared.`,
1713
}),
1814
),
1915
});
16+
export const enclaveWalletHeaderSchema = Type.Object({
17+
"x-enclave-wallet-auth-token": Type.Optional(
18+
Type.String({
19+
description:
20+
"Auth token of an enclave wallet. mutually exclusive with other wallet headers",
21+
}),
22+
),
23+
"x-client-id": Type.Optional(
24+
Type.String({
25+
description:
26+
"Client id of an enclave wallet. mutually exclusive with other wallet headers",
27+
}),
28+
),
29+
"x-ecosystem-id": Type.Optional(
30+
Type.RegExp(/^ecosystem\.[a-zA-Z0-9_-]+]$/, {
31+
description:
32+
"Ecosystem id of an enclave wallet. mutually exclusive with other wallet headers",
33+
}),
34+
),
35+
"x-ecosystem-partner-id": Type.Optional(
36+
Type.String({
37+
description:
38+
"Ecosystem partner id of an enclave wallet. mutually exclusive with other wallet headers",
39+
}),
40+
)
41+
});
42+
43+
export const walletHeaderSchema = Type.Object({
44+
"x-backend-wallet-address": {
45+
...AddressSchema,
46+
description: "Backend wallet address",
47+
},
48+
...idempotentHeaderSchema.properties,
49+
});
2050

2151
export const walletWithAAHeaderSchema = Type.Object({
2252
...walletHeaderSchema.properties,
@@ -39,6 +69,11 @@ export const walletWithAAHeaderSchema = Type.Object({
3969
),
4070
});
4171

72+
export const walletWithAAOrEnclaveHeaderSchema = Type.Object({
73+
...walletWithAAHeaderSchema.properties,
74+
...enclaveWalletHeaderSchema.properties,
75+
});
76+
4277
/**
4378
* Helper function to parse an address string.
4479
* Returns undefined if the address is undefined.

src/server/utils/convertor.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { BigNumber } from "ethers";
2+
import type { FastifyRequest} from "fastify"
3+
import type { Static } from "@sinclair/typebox";
4+
import type { enclaveWalletHeaderSchema, } from "../schemas/wallet";
5+
import type { EnclaveWalletParams } from "../../shared/utils/cache/get-enclave-wallet";
6+
import type { Ecosystem } from "thirdweb/dist/types/wallets/in-app/core/wallet/types";
7+
import type { EcosystemWalletId } from "thirdweb/dist/types/wallets/wallet-types";
28

39
const isHexBigNumber = (value: unknown) => {
410
const isNonNullObject = typeof value === "object" && value !== null;
@@ -17,3 +23,21 @@ export const bigNumberReplacer = (value: unknown): unknown => {
1723

1824
return value;
1925
};
26+
27+
export const parseEnclaveHeaders = (headers: FastifyRequest['headers']): EnclaveWalletParams => {
28+
const {
29+
"x-enclave-wallet-auth-token": authToken = "",
30+
"x-client-id": clientId = "",
31+
"x-ecosystem-id": id,
32+
"x-ecosystem-partner-id": partnerId,
33+
} = headers as Static<typeof enclaveWalletHeaderSchema>;
34+
let ecosystem: Ecosystem | undefined;
35+
if (id) {
36+
ecosystem = { id: (id as EcosystemWalletId), partnerId }
37+
}
38+
return {
39+
authToken,
40+
clientId,
41+
ecosystem,
42+
}
43+
}

src/shared/utils/account.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,23 @@ import {
1818
import { getSmartWalletV5 } from "./cache/get-smart-wallet-v5";
1919
import { getChain } from "./chain";
2020
import { thirdwebClient } from "./sdk";
21+
import { type EnclaveWalletParams, getEnclaveWalletAccount } from "./cache/get-enclave-wallet";
2122

2223
export const _accountsCache = new LRUMap<string, Account>(2048);
2324

24-
export const getAccount = async (args: {
25+
export type GetAccountArgs = {
2526
chainId: number;
26-
from: Address;
27+
from: Address | undefined;
2728
accountAddress?: Address;
28-
}): Promise<Account> => {
29-
const { chainId, from, accountAddress } = args;
29+
enclave?: EnclaveWalletParams
30+
}
31+
32+
export const getAccount = async (args: GetAccountArgs): Promise<Account> => {
33+
const { chainId, from, accountAddress, enclave } = args;
3034
const chain = await getChain(chainId);
3135

36+
if (enclave) return getEnclaveWalletAccount(enclave);
37+
if (!from) throw new Error("from is required");
3238
if (accountAddress) return getSmartWalletV5({ chain, accountAddress, from });
3339

3440
// Get from cache.
@@ -48,9 +54,9 @@ export const getAccount = async (args: {
4854
};
4955

5056
export const walletDetailsToAccount = async ({
51-
walletDetails,
52-
chain,
53-
}: {
57+
walletDetails,
58+
chain,
59+
}: {
5460
walletDetails: ParsedWalletDetails;
5561
chain: Chain;
5662
}) => {
@@ -164,9 +170,9 @@ export const _adminAccountsCache = new LRUMap<string, Account>(2048);
164170
* Will throw if the wallet is not a smart backend wallet
165171
*/
166172
export const getSmartBackendWalletAdminAccount = async ({
167-
chainId,
168-
accountAddress,
169-
}: {
173+
chainId,
174+
accountAddress,
175+
}: {
170176
chainId: number;
171177
accountAddress: Address;
172178
}) => {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { thirdwebClient as client } from "../sdk";
2+
import { EnclaveWallet } from "thirdweb/dist/types/wallets/in-app/core/wallet/enclave-wallet";
3+
import { getUserStatus } from "thirdweb/dist/types/wallets/in-app/core/actions/get-enclave-user-status";
4+
import type { Ecosystem } from "thirdweb/dist/types/wallets/in-app/core/wallet/types";
5+
import { ClientScopedStorage } from "thirdweb/dist/types/wallets/in-app/core/authentication/client-scoped-storage";
6+
import { MemoryStorage } from "./memory-storage";
7+
import type { Address } from "thirdweb";
8+
9+
export interface EnclaveWalletParams {
10+
authToken: string;
11+
clientId: string;
12+
ecosystem?: Ecosystem;
13+
}
14+
15+
const getEnclaveUserWallet = async ({ authToken, ecosystem }: EnclaveWalletParams) => {
16+
const user = await getUserStatus({ authToken, client, ecosystem });
17+
if (!user) {
18+
throw new Error("Cannot initialize wallet, no user logged in");
19+
}
20+
if (user.wallets.length === 0) {
21+
throw new Error(
22+
"Cannot initialize wallet, this user does not have a wallet generated yet",
23+
);
24+
}
25+
const wallet = user.wallets[0];
26+
if (wallet.type !== "enclave") {
27+
throw new Error(
28+
"Cannot initialize wallet, this user does not have an enclave wallet",
29+
);
30+
}
31+
return wallet;
32+
}
33+
export const getEnclaveWallet = async (
34+
{
35+
authToken,
36+
clientId,
37+
ecosystem,
38+
}: EnclaveWalletParams,
39+
) => {
40+
const wallet = await getEnclaveUserWallet({ authToken, clientId, ecosystem });
41+
return new EnclaveWallet({
42+
client,
43+
ecosystem,
44+
address: wallet.address,
45+
storage: new ClientScopedStorage({
46+
storage: new MemoryStorage(),
47+
clientId,
48+
ecosystem,
49+
}),
50+
});
51+
};
52+
53+
export const getEnclaveWalletAccount = async (params: EnclaveWalletParams) => {
54+
const wallet = await getEnclaveWallet(params);
55+
return wallet.getAccount();
56+
};
57+
58+
export const getEnclaveWalletAddress = async (params: EnclaveWalletParams) => {
59+
const wallet = await getEnclaveUserWallet(params)
60+
return wallet.address as Address;
61+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { AsyncStorage } from "@thirdweb-dev/wallets";
2+
3+
export class MemoryStorage implements AsyncStorage {
4+
data: Map<string, string> = new Map();
5+
6+
async getItem(key: string) {
7+
return this.data.get(key) || null;
8+
}
9+
10+
async setItem(key: string, value: string) {
11+
this.data.set(key, value);
12+
}
13+
14+
async removeItem(key: string) {
15+
this.data.delete(key);
16+
}
17+
}

src/shared/utils/transaction/insert-transaction.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { StatusCodes } from "http-status-codes";
22
import { randomUUID } from "node:crypto";
3-
import { TransactionDB } from "../../../shared/db/transactions/db";
3+
import { TransactionDB } from "../../db/transactions/db";
44
import {
55
getWalletDetails,
66
isSmartBackendWallet,
77
type ParsedWalletDetails,
8-
} from "../../../shared/db/wallets/get-wallet-details";
8+
} from "../../db/wallets/get-wallet-details";
99
import { doesChainSupportService } from "../../lib/chain/chain-capabilities";
1010
import { createCustomError } from "../../../server/middleware/error";
1111
import { SendTransactionQueue } from "../../../worker/queues/send-transaction-queue";
@@ -14,11 +14,13 @@ import { recordMetrics } from "../prometheus";
1414
import { reportUsage } from "../usage";
1515
import { doSimulateTransaction } from "./simulate-queued-transaction";
1616
import type { InsertedTransaction, QueuedTransaction } from "./types";
17+
import type { EnclaveWalletParams } from "../cache/get-enclave-wallet";
1718

1819
interface InsertTransactionData {
1920
insertedTransaction: InsertedTransaction;
2021
idempotencyKey?: string;
2122
shouldSimulate?: boolean;
23+
enclave?: EnclaveWalletParams;
2224
}
2325

2426
/**
@@ -30,13 +32,17 @@ interface InsertTransactionData {
3032
export const insertTransaction = async (
3133
args: InsertTransactionData,
3234
): Promise<string> => {
33-
const { insertedTransaction, idempotencyKey, shouldSimulate = false } = args;
35+
const {
36+
insertedTransaction,
37+
idempotencyKey,
38+
shouldSimulate = false,
39+
enclave
40+
} = args;
3441

3542
// The queueId uniquely represents an enqueued transaction.
3643
// It's also used as the idempotency key (default = no idempotence).
37-
let queueId: string = randomUUID();
38-
if (idempotencyKey) {
39-
queueId = idempotencyKey;
44+
const queueId: string = idempotencyKey ?? randomUUID();
45+
if (queueId === idempotencyKey) {
4046
if (await TransactionDB.exists(queueId)) {
4147
// No-op. Return the existing queueId.
4248
return queueId;
@@ -151,7 +157,7 @@ export const insertTransaction = async (
151157

152158
// Simulate the transaction.
153159
if (shouldSimulate) {
154-
const error = await doSimulateTransaction(queuedTransaction);
160+
const error = await doSimulateTransaction(queuedTransaction, enclave);
155161
if (error) {
156162
throw createCustomError(
157163
`Simulation failed: ${error.replace(/[\r\n]+/g, " --- ")}`,
@@ -165,6 +171,7 @@ export const insertTransaction = async (
165171
await SendTransactionQueue.add({
166172
queueId: queuedTransaction.queueId,
167173
resendCount: 0,
174+
enclave,
168175
});
169176
reportUsage([{ action: "queue_tx", input: queuedTransaction }]);
170177

0 commit comments

Comments
 (0)