@zoralabs/coins-sdk 0.6.0 → 0.7.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.
@@ -0,0 +1,61 @@
1
+ import { type Abi, type Address, type ContractFunctionArgs, type ContractFunctionName, type ContractFunctionParameters, type Hex } from "viem";
2
+ type WritableMutability = "payable" | "nonpayable";
3
+ type WritableFunction<abi extends Abi | readonly unknown[]> = ContractFunctionName<abi, WritableMutability>;
4
+ type WritableArgs<abi extends Abi | readonly unknown[], functionName extends WritableFunction<abi>> = ContractFunctionArgs<abi, WritableMutability, functionName>;
5
+ export type ContractCall<abi extends Abi | readonly unknown[] = readonly unknown[], fn extends WritableFunction<abi> = WritableFunction<abi>, args extends WritableArgs<abi, fn> = WritableArgs<abi, fn>> = ContractFunctionParameters<abi, WritableMutability, fn, args> & {
6
+ /** Optional ETH value to send with the call. */
7
+ value?: bigint;
8
+ /**
9
+ * Optional calldata appended after the encoded function data, e.g. for
10
+ * attribution. Mirrors viem's `dataSuffix`; concatenated by {@link toGenericCall}.
11
+ */
12
+ dataSuffix?: Hex;
13
+ };
14
+ export declare const isContractCall: (call: ContractCall | SendCall) => call is ContractCall;
15
+ export type SendCall = {
16
+ to: Address;
17
+ value?: bigint;
18
+ };
19
+ export declare const isSendCall: (call: ContractCall | SendCall) => call is SendCall;
20
+ /**
21
+ * A normalized, fully-encoded contract call.
22
+ *
23
+ * This is the canonical call shape emitted by the action `createAndValidate*Calls`
24
+ * builders. It intentionally matches the encoded-call form accepted by both
25
+ * `walletClient.sendTransaction` (EOA execution) and viem's bundler client
26
+ * `prepareUserOperation` / `sendUserOperation` (smart wallet / user operation
27
+ * execution), so a single call list can drive either flow.
28
+ */
29
+ export type GenericCall = {
30
+ to: Address;
31
+ data: Hex;
32
+ value: bigint;
33
+ };
34
+ /**
35
+ * The encoded-call shape accepted by viem's bundler client `calls` parameter
36
+ * (`prepareUserOperation` / `sendUserOperation`).
37
+ *
38
+ * `data` and `value` are optional on viem's side; we always populate them from a
39
+ * {@link GenericCall}.
40
+ */
41
+ export type UserOperationCall = {
42
+ to: Address;
43
+ data?: Hex;
44
+ value?: bigint;
45
+ };
46
+ /**
47
+ * Converts a contract call or send call to a generic call.
48
+ */
49
+ export declare function toGenericCall(call: ContractCall | SendCall): GenericCall;
50
+ /**
51
+ * Adapts a list of {@link GenericCall} into the call shape expected by viem's
52
+ * bundler client.
53
+ *
54
+ * A {@link GenericCall} is already fully encoded calldata, so this is a thin
55
+ * adapter rather than an encoder. It exists as an explicit seam: it owns any
56
+ * future divergence between our `GenericCall` shape and viem's user-operation
57
+ * call type, and keeps the conversion point obvious at call sites.
58
+ */
59
+ export declare function toUserOperationCalls(calls: GenericCall[]): UserOperationCall[];
60
+ export {};
61
+ //# sourceMappingURL=calls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calls.d.ts","sourceRoot":"","sources":["../../src/utils/calls.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,GAAG,EACR,KAAK,OAAO,EAEZ,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAE/B,KAAK,GAAG,EACT,MAAM,MAAM,CAAC;AAId,KAAK,kBAAkB,GAAG,SAAS,GAAG,YAAY,CAAC;AAEnD,KAAK,gBAAgB,CAAC,GAAG,SAAS,GAAG,GAAG,SAAS,OAAO,EAAE,IACxD,oBAAoB,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;AAEhD,KAAK,YAAY,CACf,GAAG,SAAS,GAAG,GAAG,SAAS,OAAO,EAAE,EACpC,YAAY,SAAS,gBAAgB,CAAC,GAAG,CAAC,IACxC,oBAAoB,CAAC,GAAG,EAAE,kBAAkB,EAAE,YAAY,CAAC,CAAC;AAEhE,MAAM,MAAM,YAAY,CACtB,GAAG,SAAS,GAAG,GAAG,SAAS,OAAO,EAAE,GAAG,SAAS,OAAO,EAAE,EACzD,EAAE,SAAS,gBAAgB,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,EACxD,IAAI,SAAS,YAAY,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,YAAY,CAAC,GAAG,EAAE,EAAE,CAAC,IACxD,0BAA0B,CAAC,GAAG,EAAE,kBAAkB,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG;IAClE,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,MAAM,YAAY,GAAG,QAAQ,KAC5B,IAAI,IAAI,YAMV,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,YAAY,GAAG,QAAQ,KAAG,IAAI,IAAI,QAElE,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,GAAG,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,GAAG,WAAW,CAsBxE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,WAAW,EAAE,GACnB,iBAAiB,EAAE,CAMrB"}
@@ -0,0 +1,44 @@
1
+ import type { BundlerClient, SmartAccount, UserOperation, UserOperationReceipt } from "viem/account-abstraction";
2
+ import { UserOperationCall } from "./calls";
3
+ export type PreparedUserOperation = UserOperation<"0.6">;
4
+ /**
5
+ * Prepares a user operation from a list of contract calls.
6
+ * Returns a fully-populated UserOperation (gas estimated, nonce filled) with a
7
+ * stub signature from gas estimation. Must be re-signed before submitting.
8
+ */
9
+ export declare const prepareUserOperation: ({ bundlerClient, account, calls, }: {
10
+ bundlerClient: BundlerClient;
11
+ account: SmartAccount;
12
+ calls: readonly UserOperationCall[];
13
+ }) => Promise<PreparedUserOperation>;
14
+ /**
15
+ * Signs and submits a prepared user operation, then waits for the receipt.
16
+ *
17
+ * The prepared op carries a stub signature from gas estimation, so we re-sign
18
+ * here before sending. Otherwise viem's sendUserOperation would forward the
19
+ * stub and the bundler would reject it as invalid.
20
+ */
21
+ export declare const submitUserOperation: ({ bundlerClient, account, userOperation, }: {
22
+ bundlerClient: BundlerClient;
23
+ account: SmartAccount;
24
+ userOperation: PreparedUserOperation;
25
+ }) => Promise<UserOperationReceipt>;
26
+ type CoinbaseBundlerError = {
27
+ stack: string;
28
+ message: string;
29
+ cause: unknown;
30
+ details: string;
31
+ docsPath: string;
32
+ shortMessage: string;
33
+ version: string;
34
+ name: string;
35
+ };
36
+ export declare class CoinbaseGasError extends Error {
37
+ cause: unknown;
38
+ details: string;
39
+ required?: bigint;
40
+ available?: bigint;
41
+ constructor(error: CoinbaseBundlerError);
42
+ }
43
+ export {};
44
+ //# sourceMappingURL=userOperation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userOperation.d.ts","sourceRoot":"","sources":["../../src/utils/userOperation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,aAAa,EACb,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAG5C,MAAM,MAAM,qBAAqB,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;AAEzD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAU,oCAIxC;IACD,aAAa,EAAE,aAAa,CAAC;IAC7B,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACrC,KAAG,OAAO,CAAC,qBAAqB,CAMhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAU,4CAIvC;IACD,aAAa,EAAE,aAAa,CAAC;IAC7B,OAAO,EAAE,YAAY,CAAC;IACtB,aAAa,EAAE,qBAAqB,CAAC;CACtC,KAAG,OAAO,CAAC,oBAAoB,CAmB/B,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;gBACP,KAAK,EAAE,oBAAoB;CA8BxC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/coins-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "repository": "https://github.com/ourzora/zora-protocol",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -3,23 +3,30 @@ import {
3
3
  coinFactoryABI as zoraFactoryImplABI,
4
4
  } from "@zoralabs/protocol-deployments";
5
5
  import {
6
+ Account,
6
7
  Address,
7
- TransactionReceipt,
8
- WalletClient,
9
8
  ContractEventArgsFromTopics,
10
- parseEventLogs,
9
+ decodeFunctionData,
11
10
  Hex,
12
- Account,
13
11
  isAddressEqual,
12
+ parseEventLogs,
13
+ TransactionReceipt,
14
+ WalletClient,
14
15
  } from "viem";
16
+ import { BundlerClient } from "viem/account-abstraction";
15
17
  import { base } from "viem/chains";
16
- import { validateClientNetwork } from "../utils/validateClientNetwork";
17
- import { GenericPublicClient } from "../utils/genericPublicClient";
18
+ import { postCreateContent } from "../api";
18
19
  import { validateMetadataURIContent } from "../metadata";
19
20
  import { ValidMetadataURI } from "../uploader/types";
21
+ import { GenericCall, toUserOperationCalls } from "../utils/calls";
22
+ import { GenericPublicClient } from "../utils/genericPublicClient";
20
23
  import { getChainFromId } from "../utils/getChainFromId";
21
- import { postCreateContent } from "../api";
22
24
  import { rethrowDecodedRevert } from "../utils/rethrowDecodedRevert";
25
+ import {
26
+ prepareUserOperation,
27
+ submitUserOperation,
28
+ } from "../utils/userOperation";
29
+ import { validateClientNetwork } from "../utils/validateClientNetwork";
23
30
 
24
31
  export type CoinDeploymentLogArgs = ContractEventArgsFromTopics<
25
32
  typeof zoraFactoryImplABI,
@@ -62,6 +69,13 @@ export type CreateCoinArgs = {
62
69
  additionalOwners?: Address[];
63
70
  payoutRecipientOverride?: Address;
64
71
  skipMetadataValidation?: boolean;
72
+ /**
73
+ * Enable smart wallet routing. When true, the API resolves the creator's
74
+ * linked smart wallet and returns a single call wrapped in the smart wallet's
75
+ * `execute`, so the coin is deployed and owned by the smart wallet (executed by
76
+ * an owner EOA). Used by {@link createCoinSmartWallet}. Defaults to false.
77
+ */
78
+ enableSmartWalletRouting?: boolean;
65
79
  };
66
80
 
67
81
  type TransactionParameters = {
@@ -73,6 +87,12 @@ type TransactionParameters = {
73
87
  type CreateCoinCallResponse = {
74
88
  calls: TransactionParameters[];
75
89
  predictedCoinAddress: Address;
90
+ /**
91
+ * Whether the API applied smart wallet routing. False (or undefined) means the
92
+ * call targets the factory directly (EOA creation); true means it is wrapped in
93
+ * the smart wallet's `execute`.
94
+ */
95
+ usedSmartWalletRouting?: boolean;
76
96
  };
77
97
 
78
98
  export async function createCoinCall({
@@ -86,6 +106,7 @@ export async function createCoinCall({
86
106
  additionalOwners,
87
107
  platformReferrer,
88
108
  skipMetadataValidation = false,
109
+ enableSmartWalletRouting,
89
110
  }: CreateCoinArgs): Promise<CreateCoinCallResponse> {
90
111
  // Validate metadata URI
91
112
  if (!skipMetadataValidation) {
@@ -102,6 +123,7 @@ export async function createCoinCall({
102
123
  platformReferrer,
103
124
  additionalOwners,
104
125
  payoutRecipientOverride,
126
+ enableSmartWalletRouting,
105
127
  });
106
128
 
107
129
  if (!createContentRequest.data?.calls) {
@@ -116,93 +138,201 @@ export async function createCoinCall({
116
138
  })),
117
139
  predictedCoinAddress: createContentRequest.data
118
140
  .predictedCoinAddress as Address,
141
+ usedSmartWalletRouting: createContentRequest.data.usedSmartWalletRouting,
119
142
  };
120
143
  }
121
144
 
122
145
  /**
123
- * Gets the deployed coin address from transaction receipt logs
124
- * @param receipt Transaction receipt containing the CoinCreated event
125
- * @returns The deployment information if found
146
+ * Validates the assembled calls for creating a coin.
147
+ *
148
+ * Asserts the invariants this SDK version supports: a single call, targeting the
149
+ * coin factory for the given chain, with no attached value (no buy-on-create).
150
+ * Shared by both the EOA execution path (`createCoin`) and the user-operation
151
+ * path so both validate identically.
126
152
  */
127
- export function getCoinCreateFromLogs(
128
- receipt: TransactionReceipt,
129
- ): CoinDeploymentLogArgs | undefined {
130
- const eventLogs = parseEventLogs({
131
- abi: zoraFactoryImplABI,
132
- logs: receipt.logs,
133
- });
134
-
135
- return eventLogs.find((log) => log.eventName === "CoinCreatedV4")?.args;
136
- }
137
-
138
- // Update createCoin to return both receipt and coin address
139
- export async function createCoin({
140
- call,
141
- walletClient,
142
- publicClient,
143
- options,
144
- }: {
145
- call: CreateCoinArgs;
146
- walletClient: WalletClient;
147
- publicClient: GenericPublicClient;
148
- options?: {
149
- gasMultiplier?: number;
150
- account?: Account | Address;
151
- skipValidateTransaction?: boolean;
152
- };
153
- }) {
154
- validateClientNetwork(publicClient);
155
-
156
- const chainId = call.chainId ?? publicClient.chain.id;
157
-
158
- const callRequest = await createCoinCall({
159
- ...call,
160
- chainId,
161
- });
162
-
163
- if (callRequest.calls.length !== 1) {
153
+ export function validateCreateCoinCalls(
154
+ calls: GenericCall[],
155
+ chainId: number,
156
+ ): void {
157
+ if (calls.length !== 1) {
164
158
  throw new Error("Only one call is supported for this SDK version");
165
159
  }
166
160
 
167
- const createContentCall = callRequest.calls[0];
161
+ const createContentCall = calls[0];
168
162
 
169
163
  if (!createContentCall) {
170
164
  throw new Error("Failed to load create content calldata from API");
171
165
  }
172
166
 
173
167
  const coinFactoryAddressForChain =
174
- coinFactoryAddress[call.chainId as keyof typeof coinFactoryAddress];
168
+ coinFactoryAddress[chainId as keyof typeof coinFactoryAddress];
175
169
 
176
170
  // Sanity check that the call is for the correct factory contract
177
171
  if (!isAddressEqual(createContentCall.to, coinFactoryAddressForChain)) {
178
172
  throw new Error("Creator coin is not supported for this SDK version");
179
173
  }
180
174
 
181
- // Sanity check to ensure no buy orders are sent with there parameters
175
+ // Sanity check to ensure no buy orders are sent with these parameters
182
176
  if (createContentCall.value !== 0n) {
183
177
  throw new Error(
184
178
  "Creator coin and purchase is not supported for this SDK version.",
185
179
  );
186
180
  }
181
+ }
182
+
183
+ /**
184
+ * Validates the assembled calls for creating a coin via smart wallet routing.
185
+ *
186
+ * Unlike {@link validateCreateCoinCalls}, the call targets the creator's smart
187
+ * wallet (its `execute` method), not the factory — so the factory-target check
188
+ * does not apply. The key guard is that the API actually applied routing:
189
+ * `enableSmartWalletRouting` is best-effort and silently falls back to EOA
190
+ * creation when the creator has no linked smart wallet, which must not be
191
+ * mistaken for a smart wallet creation.
192
+ */
193
+ export function validateCreateCoinSmartWalletCalls(
194
+ calls: GenericCall[],
195
+ { usedSmartWalletRouting }: { usedSmartWalletRouting?: boolean },
196
+ ): void {
197
+ if (!usedSmartWalletRouting) {
198
+ throw new Error(
199
+ "Smart wallet routing was not applied. The creator must have a linked smart wallet; otherwise use createCoin for EOA creation.",
200
+ );
201
+ }
202
+
203
+ if (calls.length !== 1) {
204
+ throw new Error("Only one call is supported for this SDK version");
205
+ }
187
206
 
188
- // Prefer a LocalAccount from the wallet client when available to ensure
189
- // offline signing (eth_sendRawTransaction) instead of wallet_sendTransaction
190
- // which can error when a `from` field is present.
191
- const selectedAccount =
192
- (typeof options?.account === "string" ? undefined : options?.account) ??
193
- walletClient.account;
207
+ const createContentCall = calls[0];
194
208
 
195
- if (!selectedAccount) {
209
+ if (!createContentCall) {
210
+ throw new Error("Failed to load create content calldata from API");
211
+ }
212
+
213
+ // Sanity check to ensure no buy orders are sent with these parameters
214
+ if (createContentCall.value !== 0n) {
215
+ throw new Error(
216
+ "Creator coin and purchase is not supported for this SDK version.",
217
+ );
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Gets the deployed coin address from transaction receipt logs
223
+ * @param receipt Transaction receipt containing the CoinCreated event
224
+ * @returns The deployment information if found
225
+ */
226
+ export function getCoinCreateFromLogs(
227
+ receipt: TransactionReceipt,
228
+ ): CoinDeploymentLogArgs | undefined {
229
+ const eventLogs = parseEventLogs({
230
+ abi: zoraFactoryImplABI,
231
+ logs: receipt.logs,
232
+ });
233
+
234
+ return eventLogs.find((log) => log.eventName === "CoinCreatedV4")?.args;
235
+ }
236
+
237
+ type CreateCoinOptions = {
238
+ gasMultiplier?: number;
239
+ account?: Account | Address;
240
+ skipValidateTransaction?: boolean;
241
+ };
242
+
243
+ /** Minimal ABI for the Coinbase Smart Wallet `execute` method, used to unwrap a routed call. */
244
+ const coinbaseSmartWalletExecuteABI = [
245
+ {
246
+ type: "function",
247
+ name: "execute",
248
+ stateMutability: "payable",
249
+ inputs: [
250
+ { name: "target", type: "address" },
251
+ { name: "value", type: "uint256" },
252
+ { name: "data", type: "bytes" },
253
+ ],
254
+ outputs: [],
255
+ },
256
+ ] as const;
257
+
258
+ /**
259
+ * Unwraps the smart wallet `execute(target, value, data)` call that the API
260
+ * returns under smart wallet routing into its inner factory call.
261
+ *
262
+ * The bundler/account (`toCoinbaseSmartAccount.encodeCalls`) re-wraps a single
263
+ * call in `execute` when forming the user operation, so the inner call — not the
264
+ * pre-wrapped one — must be fed to the user-operation path; passing the wrapped
265
+ * call would double-wrap it. The inner call still executes with the smart wallet
266
+ * as `msg.sender`, so the deployed coin's CREATE2 address is unchanged (the API's
267
+ * predicted address remains valid).
268
+ *
269
+ * Throws if the call is not a recognizable `execute` call, so an unexpected shape
270
+ * fails loudly rather than silently double-wrapping.
271
+ */
272
+ function unwrapSmartWalletExecuteCall(call: GenericCall): GenericCall {
273
+ let decoded;
274
+ try {
275
+ decoded = decodeFunctionData({
276
+ abi: coinbaseSmartWalletExecuteABI,
277
+ data: call.data,
278
+ });
279
+ } catch {
280
+ throw new Error(
281
+ "Expected a smart wallet `execute` call from smart wallet routing, but the routed call could not be decoded.",
282
+ );
283
+ }
284
+
285
+ const [target, value, data] = decoded.args;
286
+ return { to: target, value, data };
287
+ }
288
+
289
+ /**
290
+ * Selects the account used to sign and send the create transaction.
291
+ *
292
+ * Prefers a LocalAccount from the wallet client when available to ensure offline
293
+ * signing (eth_sendRawTransaction) instead of wallet_sendTransaction, which can
294
+ * error when a `from` field is present.
295
+ */
296
+ function selectExecutionAccount(
297
+ walletClient: WalletClient,
298
+ account?: Account | Address,
299
+ ): Account {
300
+ const selected =
301
+ (typeof account === "string" ? undefined : account) ?? walletClient.account;
302
+
303
+ if (!selected) {
196
304
  throw new Error("Account is required");
197
305
  }
198
306
 
307
+ return selected;
308
+ }
309
+
310
+ /**
311
+ * Simulates, gas-estimates, sends and awaits a single create call, then parses
312
+ * the deployment from the receipt logs. Shared by {@link createCoin} (factory
313
+ * call) and {@link createCoinSmartWallet} (smart wallet `execute` call) so both
314
+ * return the same shape.
315
+ */
316
+ async function executeCreateContentCall({
317
+ createContentCall,
318
+ account,
319
+ walletClient,
320
+ publicClient,
321
+ skipValidateTransaction,
322
+ }: {
323
+ createContentCall: GenericCall;
324
+ account: Account;
325
+ walletClient: WalletClient;
326
+ publicClient: GenericPublicClient;
327
+ skipValidateTransaction?: boolean;
328
+ }) {
199
329
  const viemCall = {
200
330
  ...createContentCall,
201
- account: selectedAccount,
331
+ account,
202
332
  };
203
333
 
204
334
  // simulate call
205
- if (!options?.skipValidateTransaction) {
335
+ if (!skipValidateTransaction) {
206
336
  try {
207
337
  await publicClient.call(viemCall);
208
338
  } catch (err) {
@@ -210,7 +340,7 @@ export async function createCoin({
210
340
  }
211
341
  }
212
342
 
213
- const gasEstimate = options?.skipValidateTransaction
343
+ const gasEstimate = skipValidateTransaction
214
344
  ? 10_000_000n
215
345
  : await publicClient.estimateGas(viemCall);
216
346
  const gasPrice = await publicClient.getGasPrice();
@@ -242,3 +372,127 @@ export async function createCoin({
242
372
  chain: getChainFromId(publicClient.chain.id),
243
373
  };
244
374
  }
375
+
376
+ // Update createCoin to return both receipt and coin address
377
+ export async function createCoin({
378
+ call,
379
+ walletClient,
380
+ publicClient,
381
+ options,
382
+ }: {
383
+ call: CreateCoinArgs;
384
+ walletClient: WalletClient;
385
+ publicClient: GenericPublicClient;
386
+ options?: CreateCoinOptions;
387
+ }) {
388
+ validateClientNetwork(publicClient);
389
+
390
+ const chainId = call.chainId ?? publicClient.chain.id;
391
+
392
+ const { calls } = await createCoinCall({
393
+ ...call,
394
+ chainId,
395
+ });
396
+
397
+ validateCreateCoinCalls(calls, chainId);
398
+
399
+ const createContentCall = calls[0]!;
400
+
401
+ const account = selectExecutionAccount(walletClient, options?.account);
402
+
403
+ return executeCreateContentCall({
404
+ createContentCall,
405
+ account,
406
+ walletClient,
407
+ publicClient,
408
+ skipValidateTransaction: options?.skipValidateTransaction,
409
+ });
410
+ }
411
+
412
+ /**
413
+ * Creates a coin owned by the caller's smart wallet via a user operation.
414
+ *
415
+ * Requests smart wallet routing from the API, which resolves the creator's
416
+ * linked smart wallet and returns the deploy wrapped in the smart wallet's
417
+ * `execute`. That wrapped call is unwrapped to its inner factory call and
418
+ * submitted as a user operation through `bundlerClient` — so the smart wallet
419
+ * deploys and owns the coin while gas is paid from the smart wallet's
420
+ * user-operation prefund (rather than from an owner EOA). The bundler/account
421
+ * re-wraps the inner call in `execute` itself, and the smart wallet remains
422
+ * `msg.sender`, so the deployed coin's CREATE2 address matches the API's
423
+ * prediction.
424
+ *
425
+ * Mirrors {@link createCoin}'s return shape. Throws if the API did not apply
426
+ * routing (e.g. the creator has no linked smart wallet) — use {@link createCoin}
427
+ * for EOA creation in that case.
428
+ */
429
+ export async function createCoinSmartWallet({
430
+ call,
431
+ bundlerClient,
432
+ publicClient,
433
+ }: {
434
+ call: CreateCoinArgs;
435
+ bundlerClient: BundlerClient;
436
+ publicClient: GenericPublicClient;
437
+ // `options` is accepted for signature parity with createCoin but does not
438
+ // apply to the user-operation path: the account comes from the bundler client,
439
+ // and gas/validation are handled by the bundler during preparation.
440
+ options?: CreateCoinOptions;
441
+ }) {
442
+ validateClientNetwork(publicClient);
443
+
444
+ const account = bundlerClient.account;
445
+ if (!account) {
446
+ throw new Error("Account is required: the bundler client has no account");
447
+ }
448
+
449
+ const chainId = call.chainId ?? publicClient.chain.id;
450
+
451
+ const { calls, usedSmartWalletRouting } = await createCoinCall({
452
+ ...call,
453
+ chainId,
454
+ enableSmartWalletRouting: true,
455
+ });
456
+
457
+ validateCreateCoinSmartWalletCalls(calls, { usedSmartWalletRouting });
458
+
459
+ // Unwrap the routed `execute` call so the bundler re-wraps the inner call
460
+ // itself instead of double-wrapping (see unwrapSmartWalletExecuteCall).
461
+ const innerCall = unwrapSmartWalletExecuteCall(calls[0]!);
462
+
463
+ const userOperation = await prepareUserOperation({
464
+ bundlerClient,
465
+ account,
466
+ calls: toUserOperationCalls([innerCall]),
467
+ });
468
+
469
+ const userOpReceipt = await submitUserOperation({
470
+ bundlerClient,
471
+ account,
472
+ userOperation,
473
+ });
474
+
475
+ if (!userOpReceipt.success) {
476
+ throw new Error(
477
+ `User operation reverted${userOpReceipt.reason ? `: ${userOpReceipt.reason}` : ""}`,
478
+ );
479
+ }
480
+
481
+ // Parse the deployment from this user operation's own logs (not the whole
482
+ // bundle's), so a co-bundled CoinCreatedV4 can't be misattributed.
483
+ const eventLogs = parseEventLogs({
484
+ abi: zoraFactoryImplABI,
485
+ logs: userOpReceipt.logs,
486
+ });
487
+ const deployment = eventLogs.find(
488
+ (log) => log.eventName === "CoinCreatedV4",
489
+ )?.args;
490
+
491
+ return {
492
+ hash: userOpReceipt.receipt.transactionHash,
493
+ receipt: userOpReceipt.receipt,
494
+ address: deployment?.coin,
495
+ deployment,
496
+ chain: getChainFromId(publicClient.chain.id),
497
+ };
498
+ }