alchemy 0.69.1 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/bin/alchemy.js +3797 -3857
  2. package/bin/trpc.ts +4 -9
  3. package/lib/alchemy.d.ts +3 -3
  4. package/lib/alchemy.d.ts.map +1 -1
  5. package/lib/alchemy.js +2 -15
  6. package/lib/alchemy.js.map +1 -1
  7. package/lib/apply.js +24 -9
  8. package/lib/apply.js.map +1 -1
  9. package/lib/aws/utils.d.ts +2 -1
  10. package/lib/aws/utils.d.ts.map +1 -1
  11. package/lib/aws/utils.js.map +1 -1
  12. package/lib/clickhouse/api/client.gen.d.ts +13 -0
  13. package/lib/clickhouse/api/client.gen.d.ts.map +1 -0
  14. package/lib/clickhouse/api/client.gen.js +7 -0
  15. package/lib/clickhouse/api/client.gen.js.map +1 -0
  16. package/lib/clickhouse/api/sdk.gen.d.ts +322 -0
  17. package/lib/clickhouse/api/sdk.gen.d.ts.map +1 -0
  18. package/lib/clickhouse/api/sdk.gen.js +683 -0
  19. package/lib/clickhouse/api/sdk.gen.js.map +1 -0
  20. package/lib/clickhouse/api/types.gen.d.ts +5030 -0
  21. package/lib/clickhouse/api/types.gen.d.ts.map +1 -0
  22. package/lib/clickhouse/api/types.gen.js +2 -0
  23. package/lib/clickhouse/api/types.gen.js.map +1 -0
  24. package/lib/clickhouse/api-key.d.ts +29 -0
  25. package/lib/clickhouse/api-key.d.ts.map +1 -0
  26. package/lib/clickhouse/api-key.js +83 -0
  27. package/lib/clickhouse/api-key.js.map +1 -0
  28. package/lib/clickhouse/api.d.ts +36 -0
  29. package/lib/clickhouse/api.d.ts.map +1 -0
  30. package/lib/clickhouse/api.js +48 -0
  31. package/lib/clickhouse/api.js.map +1 -0
  32. package/lib/clickhouse/index.d.ts +4 -0
  33. package/lib/clickhouse/index.d.ts.map +1 -0
  34. package/lib/clickhouse/index.js +4 -0
  35. package/lib/clickhouse/index.js.map +1 -0
  36. package/lib/clickhouse/organization.d.ts +16 -0
  37. package/lib/clickhouse/organization.d.ts.map +1 -0
  38. package/lib/clickhouse/organization.js +21 -0
  39. package/lib/clickhouse/organization.js.map +1 -0
  40. package/lib/clickhouse/service.d.ts +275 -0
  41. package/lib/clickhouse/service.d.ts.map +1 -0
  42. package/lib/clickhouse/service.js +274 -0
  43. package/lib/clickhouse/service.js.map +1 -0
  44. package/lib/cloudflare/account-api-token.d.ts.map +1 -1
  45. package/lib/cloudflare/account-api-token.js +1 -1
  46. package/lib/cloudflare/account-api-token.js.map +1 -1
  47. package/lib/cloudflare/bun-spa/bun-spa.d.ts +15 -0
  48. package/lib/cloudflare/bun-spa/bun-spa.d.ts.map +1 -0
  49. package/lib/cloudflare/bun-spa/bun-spa.js +95 -0
  50. package/lib/cloudflare/bun-spa/bun-spa.js.map +1 -0
  51. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  52. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  53. package/lib/cloudflare/container.d.ts +11 -1
  54. package/lib/cloudflare/container.d.ts.map +1 -1
  55. package/lib/cloudflare/container.js.map +1 -1
  56. package/lib/cloudflare/index.d.ts +1 -0
  57. package/lib/cloudflare/index.d.ts.map +1 -1
  58. package/lib/cloudflare/index.js +1 -0
  59. package/lib/cloudflare/index.js.map +1 -1
  60. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  61. package/lib/cloudflare/miniflare/build-worker-options.js +3 -3
  62. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  63. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  64. package/lib/cloudflare/miniflare/miniflare-controller.js +2 -0
  65. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  66. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +1 -1
  67. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  68. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +36 -49
  69. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  70. package/lib/cloudflare/permission-groups.d.ts +2 -3
  71. package/lib/cloudflare/permission-groups.d.ts.map +1 -1
  72. package/lib/cloudflare/permission-groups.js +10 -15
  73. package/lib/cloudflare/permission-groups.js.map +1 -1
  74. package/lib/cloudflare/redwood/redwood.d.ts +1 -1
  75. package/lib/cloudflare/redwood/redwood.js +1 -1
  76. package/lib/cloudflare/util/filter-env-bindings.d.ts +62 -0
  77. package/lib/cloudflare/util/filter-env-bindings.d.ts.map +1 -0
  78. package/lib/cloudflare/util/filter-env-bindings.js +81 -0
  79. package/lib/cloudflare/util/filter-env-bindings.js.map +1 -0
  80. package/lib/cloudflare/website.d.ts +2 -2
  81. package/lib/cloudflare/website.d.ts.map +1 -1
  82. package/lib/cloudflare/website.js +6 -18
  83. package/lib/cloudflare/website.js.map +1 -1
  84. package/lib/cloudflare/worker-metadata.d.ts +1 -0
  85. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  86. package/lib/cloudflare/worker-metadata.js +1 -0
  87. package/lib/cloudflare/worker-metadata.js.map +1 -1
  88. package/lib/cloudflare/worker.d.ts +11 -1
  89. package/lib/cloudflare/worker.d.ts.map +1 -1
  90. package/lib/cloudflare/worker.js.map +1 -1
  91. package/lib/cloudflare/wrangler.json.d.ts +8 -0
  92. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  93. package/lib/cloudflare/wrangler.json.js +3 -4
  94. package/lib/cloudflare/wrangler.json.js.map +1 -1
  95. package/lib/coinbase/client.d.ts +54 -0
  96. package/lib/coinbase/client.d.ts.map +1 -0
  97. package/lib/coinbase/client.js +89 -0
  98. package/lib/coinbase/client.js.map +1 -0
  99. package/lib/coinbase/evm-account.d.ts +118 -0
  100. package/lib/coinbase/evm-account.d.ts.map +1 -0
  101. package/lib/coinbase/evm-account.js +198 -0
  102. package/lib/coinbase/evm-account.js.map +1 -0
  103. package/lib/coinbase/evm-smart-account.d.ts +122 -0
  104. package/lib/coinbase/evm-smart-account.d.ts.map +1 -0
  105. package/lib/coinbase/evm-smart-account.js +213 -0
  106. package/lib/coinbase/evm-smart-account.js.map +1 -0
  107. package/lib/coinbase/faucet.d.ts +11 -0
  108. package/lib/coinbase/faucet.d.ts.map +1 -0
  109. package/lib/coinbase/faucet.js +146 -0
  110. package/lib/coinbase/faucet.js.map +1 -0
  111. package/lib/coinbase/index.d.ts +4 -0
  112. package/lib/coinbase/index.d.ts.map +1 -0
  113. package/lib/coinbase/index.js +4 -0
  114. package/lib/coinbase/index.js.map +1 -0
  115. package/lib/coinbase/types.d.ts +28 -0
  116. package/lib/coinbase/types.d.ts.map +1 -0
  117. package/lib/coinbase/types.js +4 -0
  118. package/lib/{util/telemetry → coinbase}/types.js.map +1 -1
  119. package/lib/coinbase/utils.d.ts +13 -0
  120. package/lib/coinbase/utils.d.ts.map +1 -0
  121. package/lib/coinbase/utils.js +17 -0
  122. package/lib/coinbase/utils.js.map +1 -0
  123. package/lib/neon/api/types.gen.d.ts +7 -2
  124. package/lib/neon/api/types.gen.d.ts.map +1 -1
  125. package/lib/planetscale/api/types.gen.d.ts +20 -4
  126. package/lib/planetscale/api/types.gen.d.ts.map +1 -1
  127. package/lib/planetscale/api.d.ts.map +1 -1
  128. package/lib/planetscale/api.js +1 -0
  129. package/lib/planetscale/api.js.map +1 -1
  130. package/lib/planetscale/password.d.ts.map +1 -1
  131. package/lib/planetscale/password.js +1 -13
  132. package/lib/planetscale/password.js.map +1 -1
  133. package/lib/resource.d.ts.map +1 -1
  134. package/lib/resource.js +7 -3
  135. package/lib/resource.js.map +1 -1
  136. package/lib/scope.d.ts +4 -9
  137. package/lib/scope.d.ts.map +1 -1
  138. package/lib/scope.js +16 -37
  139. package/lib/scope.js.map +1 -1
  140. package/lib/state/instrumented-state-store.d.ts +1 -3
  141. package/lib/state/instrumented-state-store.d.ts.map +1 -1
  142. package/lib/state/instrumented-state-store.js +19 -21
  143. package/lib/state/instrumented-state-store.js.map +1 -1
  144. package/lib/test/bun.d.ts.map +1 -1
  145. package/lib/test/bun.js +1 -2
  146. package/lib/test/bun.js.map +1 -1
  147. package/lib/test/vitest.d.ts.map +1 -1
  148. package/lib/test/vitest.js +1 -2
  149. package/lib/test/vitest.js.map +1 -1
  150. package/lib/util/diff.d.ts +5 -0
  151. package/lib/util/diff.d.ts.map +1 -0
  152. package/lib/util/diff.js +14 -0
  153. package/lib/util/diff.js.map +1 -0
  154. package/lib/util/telemetry.d.ts +52 -0
  155. package/lib/util/telemetry.d.ts.map +1 -0
  156. package/lib/util/telemetry.js +191 -0
  157. package/lib/util/telemetry.js.map +1 -0
  158. package/package.json +19 -1
  159. package/src/alchemy.ts +5 -20
  160. package/src/apply.ts +29 -12
  161. package/src/aws/utils.ts +2 -1
  162. package/src/clickhouse/api/client.gen.ts +28 -0
  163. package/src/clickhouse/api/sdk.gen.ts +1286 -0
  164. package/src/clickhouse/api/types.gen.ts +5898 -0
  165. package/src/clickhouse/api-key.ts +136 -0
  166. package/src/clickhouse/api.ts +84 -0
  167. package/src/clickhouse/index.ts +3 -0
  168. package/src/clickhouse/organization.ts +31 -0
  169. package/src/clickhouse/service.ts +668 -0
  170. package/src/cloudflare/account-api-token.ts +1 -4
  171. package/src/cloudflare/bun-spa/bun-spa.ts +139 -0
  172. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  173. package/src/cloudflare/container.ts +20 -1
  174. package/src/cloudflare/index.ts +1 -0
  175. package/src/cloudflare/miniflare/build-worker-options.ts +6 -4
  176. package/src/cloudflare/miniflare/miniflare-controller.ts +2 -0
  177. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +41 -58
  178. package/src/cloudflare/permission-groups.ts +21 -49
  179. package/src/cloudflare/redwood/redwood.ts +1 -1
  180. package/src/cloudflare/util/filter-env-bindings.ts +96 -0
  181. package/src/cloudflare/website.ts +11 -24
  182. package/src/cloudflare/worker-metadata.ts +2 -0
  183. package/src/cloudflare/worker.ts +12 -1
  184. package/src/cloudflare/wrangler.json.ts +12 -8
  185. package/src/coinbase/client.ts +146 -0
  186. package/src/coinbase/evm-account.ts +317 -0
  187. package/src/coinbase/evm-smart-account.ts +339 -0
  188. package/src/coinbase/faucet.ts +173 -0
  189. package/src/coinbase/index.ts +3 -0
  190. package/src/coinbase/types.ts +33 -0
  191. package/src/coinbase/utils.ts +19 -0
  192. package/src/neon/api/types.gen.ts +13 -1
  193. package/src/planetscale/api/types.gen.ts +22 -2
  194. package/src/planetscale/api.ts +1 -0
  195. package/src/planetscale/password.ts +1 -14
  196. package/src/resource.ts +11 -4
  197. package/src/scope.ts +22 -45
  198. package/src/state/instrumented-state-store.ts +27 -26
  199. package/src/test/bun.ts +1 -2
  200. package/src/test/vitest.ts +1 -2
  201. package/src/util/diff.ts +14 -0
  202. package/src/util/telemetry.ts +297 -0
  203. package/workers/tunnel-proxy.js +1 -1
  204. package/lib/util/telemetry/client.d.ts +0 -31
  205. package/lib/util/telemetry/client.d.ts.map +0 -1
  206. package/lib/util/telemetry/client.js +0 -121
  207. package/lib/util/telemetry/client.js.map +0 -1
  208. package/lib/util/telemetry/constants.d.ts +0 -5
  209. package/lib/util/telemetry/constants.d.ts.map +0 -1
  210. package/lib/util/telemetry/constants.js +0 -8
  211. package/lib/util/telemetry/constants.js.map +0 -1
  212. package/lib/util/telemetry/context.d.ts +0 -15
  213. package/lib/util/telemetry/context.d.ts.map +0 -1
  214. package/lib/util/telemetry/context.js +0 -128
  215. package/lib/util/telemetry/context.js.map +0 -1
  216. package/lib/util/telemetry/index.d.ts +0 -3
  217. package/lib/util/telemetry/index.d.ts.map +0 -1
  218. package/lib/util/telemetry/index.js +0 -3
  219. package/lib/util/telemetry/index.js.map +0 -1
  220. package/lib/util/telemetry/types.d.ts +0 -68
  221. package/lib/util/telemetry/types.d.ts.map +0 -1
  222. package/lib/util/telemetry/types.js +0 -1
  223. package/src/util/telemetry/client.ts +0 -156
  224. package/src/util/telemetry/constants.ts +0 -13
  225. package/src/util/telemetry/context.ts +0 -154
  226. package/src/util/telemetry/index.ts +0 -2
  227. package/src/util/telemetry/types.ts +0 -105
@@ -179,6 +179,17 @@ export interface BaseWorkerProps<
179
179
  enabled?: boolean;
180
180
  };
181
181
 
182
+ /**
183
+ * Enable Workers Logpush to export trace events (request/response metadata,
184
+ * console logs, and exceptions) to external destinations.
185
+ *
186
+ * Requires a separate Logpush job configuration via the Cloudflare API.
187
+ *
188
+ * @see https://developers.cloudflare.com/workers/observability/logging/logpush
189
+ * @default false
190
+ */
191
+ logpush?: boolean;
192
+
182
193
  /**
183
194
  * Whether to adopt the Worker if it already exists when creating
184
195
  */
@@ -324,7 +335,7 @@ export interface BaseWorkerProps<
324
335
  dev?:
325
336
  | {
326
337
  /**
327
- * Port to use for local development
338
+ * Port to use for local development.
328
339
  */
329
340
  port?: number;
330
341
  /**
@@ -4,6 +4,7 @@ import type { Context } from "../context.ts";
4
4
  import { Resource } from "../resource.ts";
5
5
  import { Scope } from "../scope.ts";
6
6
  import { isSecret } from "../secret.ts";
7
+ import { unencryptSecrets } from "./util/filter-env-bindings.ts";
7
8
  import { assertNever } from "../util/assert-never.ts";
8
9
  import type { Bindings, WorkerBindingRateLimit } from "./bindings.ts";
9
10
  import type { R2BucketJurisdiction } from "./bucket.ts";
@@ -162,6 +163,7 @@ export async function WranglerJson(
162
163
  : undefined,
163
164
  placement: worker.placement,
164
165
  limits: worker.limits,
166
+ logpush: worker.logpush,
165
167
  };
166
168
 
167
169
  // Process bindings if they exist
@@ -210,14 +212,7 @@ export async function WranglerJson(
210
212
  // but do not modify `finalSpec` so that way secrets aren't written to state unencrypted.
211
213
  const withSecretsUnwrapped = {
212
214
  ...finalSpec,
213
- vars: {
214
- ...finalSpec.vars,
215
- ...Object.fromEntries(
216
- Object.entries(finalSpec.vars ?? {}).map(([key, value]) =>
217
- isSecret(value) ? [key, value.unencrypted] : [key, value],
218
- ),
219
- ),
220
- },
215
+ vars: unencryptSecrets(finalSpec.vars ?? {}),
221
216
  };
222
217
  await writeJSON(filePath, withSecretsUnwrapped);
223
218
  } else {
@@ -275,6 +270,15 @@ export interface WranglerJsonSpec {
275
270
  cpu_ms?: number;
276
271
  };
277
272
 
273
+ /**
274
+ * Send Trace Events from this Worker to Workers Logpush.
275
+ *
276
+ * This will not configure a corresponding Logpush job automatically.
277
+ *
278
+ * @default false
279
+ */
280
+ logpush?: boolean;
281
+
278
282
  /**
279
283
  * Whether to enable a workers.dev URL for this worker
280
284
  */
@@ -0,0 +1,146 @@
1
+ import type { CdpClient } from "@coinbase/cdp-sdk";
2
+ import type { Secret } from "../secret.ts";
3
+ import { logger } from "../util/logger.ts";
4
+ import { importPeer } from "../util/peer.ts";
5
+
6
+ export interface CoinbaseClientOptions {
7
+ /**
8
+ * CDP API Key ID. If not provided, falls back to CDP_API_KEY_ID environment variable.
9
+ */
10
+ apiKeyId?: Secret<string> | string;
11
+ /**
12
+ * CDP API Key Secret. If not provided, falls back to CDP_API_KEY_SECRET environment variable.
13
+ * Must be wrapped in alchemy.secret() for security.
14
+ */
15
+ apiKeySecret?: Secret<string>;
16
+ /**
17
+ * CDP Wallet Secret. If not provided, falls back to CDP_WALLET_SECRET environment variable.
18
+ * Must be wrapped in alchemy.secret() for security.
19
+ */
20
+ walletSecret?: Secret<string>;
21
+ }
22
+
23
+ /**
24
+ * Create an authenticated CDP client
25
+ *
26
+ * The CDP SDK automatically looks for these environment variables:
27
+ * - CDP_API_KEY_ID
28
+ * - CDP_API_KEY_SECRET
29
+ * - CDP_WALLET_SECRET
30
+ *
31
+ * You can override them by passing values in the options.
32
+ *
33
+ * @param options Options for creating the client
34
+ * @returns An authenticated CDP client
35
+ */
36
+ export async function createCdpClient(
37
+ options: CoinbaseClientOptions = {},
38
+ ): Promise<CdpClient> {
39
+ const { CdpClient } = await importPeer(
40
+ import("@coinbase/cdp-sdk"),
41
+ "Coinbase resources",
42
+ );
43
+
44
+ // Build options object for CdpClient constructor
45
+ const clientOptions: {
46
+ apiKeyId?: string;
47
+ apiKeySecret?: string;
48
+ walletSecret?: string;
49
+ } = {};
50
+
51
+ // Only set values if explicitly provided, otherwise let CDP SDK use its defaults
52
+ if (options.apiKeyId) {
53
+ clientOptions.apiKeyId =
54
+ typeof options.apiKeyId === "string"
55
+ ? options.apiKeyId
56
+ : options.apiKeyId.unencrypted;
57
+ }
58
+
59
+ if (options.apiKeySecret) {
60
+ clientOptions.apiKeySecret = options.apiKeySecret.unencrypted;
61
+ }
62
+
63
+ if (options.walletSecret) {
64
+ clientOptions.walletSecret = options.walletSecret.unencrypted;
65
+ }
66
+
67
+ // CDP SDK will automatically use environment variables if not provided:
68
+ // CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET
69
+ return new CdpClient(clientOptions);
70
+ }
71
+
72
+ /**
73
+ * Handle CDP SDK deletion errors gracefully
74
+ *
75
+ * @param error The error from CDP SDK
76
+ * @param resourceType The type of resource being deleted
77
+ * @param resourceId The ID or name of the resource
78
+ */
79
+ export function handleCdpDeleteError(
80
+ _error: any,
81
+ resourceType: string,
82
+ resourceId?: string,
83
+ ): void {
84
+ // CDP SDK doesn't support deleting accounts, so we just log
85
+ logger.log(
86
+ `${resourceType} ${resourceId || "unknown"} cannot be deleted from CDP (CDP limitation)`,
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Check if an error indicates the resource already exists
92
+ *
93
+ * @param error The error from CDP SDK
94
+ * @returns True if the error indicates a conflict/already exists
95
+ */
96
+ export function isCdpConflictError(error: any): boolean {
97
+ return (
98
+ error?.message?.toLowerCase()?.includes("already exists") ||
99
+ error?.message?.toLowerCase()?.includes("duplicate") ||
100
+ error?.code === "ALREADY_EXISTS" ||
101
+ error?.code === "CONFLICT"
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Verifies CDP authentication and provides helpful error messages
107
+ *
108
+ * @param client CDP client
109
+ */
110
+ export async function verifyCdpAuth(client: CdpClient): Promise<void> {
111
+ try {
112
+ // Make a test request to check authentication
113
+ // List accounts is a good test as it requires valid credentials
114
+ await client.evm.listAccounts();
115
+ } catch (error: any) {
116
+ if (error.errorType === "unauthorized") {
117
+ logger.error(
118
+ "\n⚠️ Coinbase CDP authentication failed. Please check your credentials:",
119
+ );
120
+ logger.error("1. Ensure CDP_API_KEY_ID is set with your API Key ID");
121
+ logger.error(
122
+ "2. Ensure CDP_API_KEY_SECRET is set with your API Key Secret",
123
+ );
124
+ logger.error(
125
+ "3. Ensure CDP_WALLET_SECRET is set with your Wallet Secret",
126
+ );
127
+ logger.error(
128
+ "\nTo create API credentials, visit: https://portal.cdp.coinbase.com/",
129
+ );
130
+ logger.error(
131
+ "Required permissions: Account management and transaction signing\n",
132
+ );
133
+ throw new Error("CDP authentication failed");
134
+ }
135
+ if (error.errorType === "unauthorized") {
136
+ logger.error(
137
+ "\n⚠️ Insufficient permissions. Your API key needs account management permissions.",
138
+ );
139
+ logger.error(
140
+ "Make sure your API key has the required scopes for creating and managing accounts\n",
141
+ );
142
+ throw new Error("Insufficient CDP permissions");
143
+ }
144
+ throw error;
145
+ }
146
+ }
@@ -0,0 +1,317 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { Secret } from "../index.ts";
3
+ import { Resource } from "../resource.ts";
4
+ import { createCdpClient, type CoinbaseClientOptions } from "./client.ts";
5
+ import type {
6
+ Address,
7
+ FaucetConfig,
8
+ FaucetNetwork,
9
+ FaucetToken,
10
+ PrivateKey,
11
+ } from "./types.ts";
12
+ import { validateAccountName } from "./utils.ts";
13
+
14
+ // Re-export types for backward compatibility
15
+ export type { FaucetConfig, FaucetNetwork, FaucetToken } from "./types.ts";
16
+
17
+ export interface EvmAccountProps extends CoinbaseClientOptions {
18
+ /**
19
+ * Name for the account.
20
+ * Used for identification in CDP.
21
+ * Must contain only letters, numbers, and hyphens.
22
+ */
23
+ name: string;
24
+ /**
25
+ * Optional private key to import an existing account.
26
+ * Must be a hex string (starting with 0x) encrypted using alchemy.secret().
27
+ * If not provided, a new account will be created or existing one will be used.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * privateKey: alchemy.secret(process.env.PRIVATE_KEY)
32
+ * ```
33
+ */
34
+ privateKey?: Secret<PrivateKey>;
35
+ /**
36
+ * Whether to adopt an existing account with the same name if it already exists.
37
+ * Without adoption, creation will fail if an account with the same name exists.
38
+ * With adoption, the existing account will be used.
39
+ * @default false
40
+ */
41
+ adopt?: boolean;
42
+ /**
43
+ * Faucet configuration for development funding.
44
+ * Declares which tokens this account should have.
45
+ * Used by external funding scripts - not processed by the resource.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * faucet: {
50
+ * "base-sepolia": ["eth", "usdc"],
51
+ * "ethereum-sepolia": ["eth"]
52
+ * }
53
+ * ```
54
+ */
55
+ faucet?: FaucetConfig;
56
+ }
57
+
58
+ export interface EvmAccount extends Resource<"coinbase::evm-account"> {
59
+ /**
60
+ * The account name in CDP
61
+ */
62
+ name: string;
63
+ /**
64
+ * The EVM address (same across all EVM networks)
65
+ */
66
+ address: Address;
67
+ /**
68
+ * Faucet configuration (passed through from props)
69
+ */
70
+ faucet?: FaucetConfig;
71
+ }
72
+
73
+ /**
74
+ * Manages EVM EOA (Externally Owned Accounts) on Coinbase Developer Platform.
75
+ *
76
+ * @example
77
+ * ## Create a new EVM account
78
+ *
79
+ * ```ts
80
+ * const account = await EvmAccount("my-account", {
81
+ * name: "my-account"
82
+ * });
83
+ *
84
+ * console.log("Account address:", account.address);
85
+ * ```
86
+ *
87
+ * @example
88
+ * ## Create account with funding metadata
89
+ *
90
+ * Declare what tokens this account needs for development
91
+ *
92
+ * ```ts
93
+ * const account = await EvmAccount("test-account", {
94
+ * name: "test-account",
95
+ * faucet: {
96
+ * "base-sepolia": ["eth", "usdc"],
97
+ * "ethereum-sepolia": ["eth"]
98
+ * }
99
+ * });
100
+ *
101
+ * // Account automatically requests tokens on creation
102
+ * // The faucet script can be used for bulk funding operations
103
+ * ```
104
+ *
105
+ * @example
106
+ * ## Import an existing account
107
+ *
108
+ * Import an existing EVM account using a private key
109
+ *
110
+ * ```ts
111
+ * const account = await EvmAccount("imported-account", {
112
+ * name: "imported-account",
113
+ * privateKey: alchemy.secret(process.env.COINBASE_PRIVATE_KEY)
114
+ * });
115
+ * ```
116
+ *
117
+ * @example
118
+ * ## Adopt an existing account
119
+ *
120
+ * Use an existing account with the same name if it exists
121
+ *
122
+ * ```ts
123
+ * const account = await EvmAccount("my-account", {
124
+ * name: "existing-account",
125
+ * adopt: true // Uses existing account if it exists
126
+ * });
127
+ * ```
128
+ */
129
+ export const EvmAccount = Resource(
130
+ "coinbase::evm-account",
131
+ async function (
132
+ this: Context<EvmAccount>,
133
+ _id: string,
134
+ props: EvmAccountProps,
135
+ ): Promise<EvmAccount> {
136
+ // Validate account name format
137
+ validateAccountName(props.name);
138
+
139
+ // Initialize CDP client with credentials from props or environment
140
+ const cdp = await createCdpClient({
141
+ apiKeyId: props.apiKeyId,
142
+ apiKeySecret: props.apiKeySecret,
143
+ walletSecret: props.walletSecret,
144
+ });
145
+
146
+ // Handle update phase
147
+ if (this.phase === "update" && this.output) {
148
+ // Only name can be updated
149
+ if (props.name !== this.output.name) {
150
+ // CDP SDK supports updating account names via updateAccount
151
+ await cdp.evm.updateAccount({
152
+ address: this.output.address,
153
+ update: {
154
+ name: props.name,
155
+ },
156
+ });
157
+ return {
158
+ ...this.output,
159
+ name: props.name, // Use the requested name
160
+ faucet: props.faucet,
161
+ };
162
+ }
163
+
164
+ // Update faucet configuration if changed
165
+ if (JSON.stringify(props.faucet) !== JSON.stringify(this.output.faucet)) {
166
+ // Only fund NEW combinations that weren't in the previous config
167
+ try {
168
+ // Get all old combinations
169
+ const oldCombinations = new Set(
170
+ Object.entries(this.output.faucet ?? {}).flatMap(
171
+ ([network, tokens]) =>
172
+ tokens.map((token) => `${network}:${token}`),
173
+ ),
174
+ );
175
+
176
+ // Get all new combinations
177
+ const newCombinations = Object.entries(props.faucet ?? {}).flatMap(
178
+ ([network, tokens]) =>
179
+ tokens.map((token) => ({
180
+ network: network as FaucetNetwork,
181
+ token: token as FaucetToken,
182
+ key: `${network}:${token}`,
183
+ })),
184
+ );
185
+
186
+ // Filter to only combinations that are actually new
187
+ const toFund = newCombinations.filter(
188
+ ({ key }) => !oldCombinations.has(key),
189
+ );
190
+
191
+ if (toFund.length > 0) {
192
+ await Promise.all(
193
+ toFund.map(({ network, token }) =>
194
+ cdp.evm
195
+ .requestFaucet({
196
+ address: this.output.address,
197
+ network,
198
+ token,
199
+ })
200
+ .catch((err) => {
201
+ console.warn(
202
+ `⚠️ Failed to request ${token} on ${network} for ${this.output.address}: ${err.message}`,
203
+ );
204
+ return null;
205
+ }),
206
+ ),
207
+ );
208
+
209
+ console.log(
210
+ `💧 Requested ${toFund.length} new faucet fund${toFund.length === 1 ? "" : "s"} for ${this.output.address}`,
211
+ );
212
+ }
213
+ } catch (error: any) {
214
+ console.warn(
215
+ `⚠️ Failed to request additional faucet funds for ${this.output.address}: ${error.message}`,
216
+ );
217
+ // Continue - don't break the update
218
+ }
219
+
220
+ return {
221
+ ...this.output,
222
+ faucet: props.faucet,
223
+ };
224
+ }
225
+
226
+ return this.output;
227
+ }
228
+
229
+ // Handle delete phase
230
+ if (this.phase === "delete") {
231
+ // CDP SDK doesn't support deleting accounts
232
+ // Accounts remain in CDP but are no longer tracked by Alchemy
233
+ console.log(`🗑️ Untracking EVM Account: ${this.output.address}`);
234
+ return this.destroy();
235
+ }
236
+
237
+ let account;
238
+
239
+ // Handle account creation/retrieval
240
+ if (props.privateKey) {
241
+ // Import account with private key
242
+ // This is idempotent in CDP
243
+ account = await cdp.evm.importAccount({
244
+ privateKey: props.privateKey.unencrypted,
245
+ name: props.name,
246
+ });
247
+ } else {
248
+ // Check for adoption or use getOrCreate pattern
249
+ const adopt = props.adopt ?? this.scope.adopt;
250
+ if (adopt) {
251
+ // With adoption, use getOrCreate which returns existing if it exists
252
+ account = await cdp.evm.getOrCreateAccount({
253
+ name: props.name,
254
+ });
255
+ } else {
256
+ // Without adoption, we can attempt to create it.
257
+ // It will throw if already exist
258
+ try {
259
+ account = await cdp.evm.createAccount({ name: props.name });
260
+ } catch (error: any) {
261
+ // Provide helpful error message if account already exists
262
+ if (error.errorType === "already_exists") {
263
+ throw new Error(
264
+ `Account with name '${props.name}' already exists. Use adopt: true to use the existing account.`,
265
+ );
266
+ }
267
+ // Rethrow other errors unchanged
268
+ throw error;
269
+ }
270
+ }
271
+ }
272
+
273
+ // Fund the account if faucet configuration is provided
274
+ if (props.faucet) {
275
+ try {
276
+ const combinations = Object.entries(props.faucet).flatMap(
277
+ ([network, tokens]) =>
278
+ tokens.map((token) => ({
279
+ network: network as FaucetNetwork,
280
+ token: token as FaucetToken,
281
+ })),
282
+ );
283
+
284
+ await Promise.all(
285
+ combinations.map(({ network, token }) =>
286
+ cdp.evm
287
+ .requestFaucet({
288
+ address: account.address,
289
+ network,
290
+ token,
291
+ })
292
+ .catch((err) => {
293
+ console.warn(
294
+ `⚠️ Failed to request ${token} on ${network} for ${account.address}: ${err.message}`,
295
+ );
296
+ return null;
297
+ }),
298
+ ),
299
+ );
300
+
301
+ console.log(`💧 Requested faucet funds for ${account.address}`);
302
+ } catch (error: any) {
303
+ console.warn(
304
+ `⚠️ Failed to request faucet funds for ${account.address}: ${error.message}`,
305
+ );
306
+ // Continue - don't break account creation
307
+ }
308
+ }
309
+
310
+ // Return account details
311
+ return {
312
+ name: props.name, // Always use the name from props for consistency
313
+ address: account.address,
314
+ faucet: props.faucet,
315
+ } as EvmAccount;
316
+ },
317
+ );