@zoralabs/coins-sdk 0.6.0 → 0.7.1
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.
- package/CHANGELOG.md +20 -0
- package/dist/actions/createCoin.d.ts +92 -7
- package/dist/actions/createCoin.d.ts.map +1 -1
- package/dist/actions/tradeCoin.d.ts +27 -2
- package/dist/actions/tradeCoin.d.ts.map +1 -1
- package/dist/actions/updateCoinURI.d.ts +37 -1
- package/dist/actions/updateCoinURI.d.ts.map +1 -1
- package/dist/actions/updatePayoutRecipient.d.ts +43 -1
- package/dist/actions/updatePayoutRecipient.d.ts.map +1 -1
- package/dist/api/api-raw.d.ts +1 -0
- package/dist/api/api-raw.d.ts.map +1 -1
- package/dist/index.cjs +655 -243
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +639 -227
- package/dist/index.js.map +1 -1
- package/dist/metadata/validateMetadataURIContent.d.ts.map +1 -1
- package/dist/utils/calls.d.ts +61 -0
- package/dist/utils/calls.d.ts.map +1 -0
- package/dist/utils/userOperation.d.ts +44 -0
- package/dist/utils/userOperation.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/actions/createCoin.ts +314 -60
- package/src/actions/tradeCoin.ts +237 -72
- package/src/actions/updateCoinURI.ts +84 -6
- package/src/actions/updatePayoutRecipient.ts +92 -5
- package/src/api/api-raw.test.ts +61 -0
- package/src/api/api-raw.ts +9 -0
- package/src/index.ts +41 -3
- package/src/metadata/validateMetadataURIContent.ts +17 -2
- package/src/utils/calls.ts +129 -0
- package/src/utils/userOperation.test.ts +84 -0
- package/src/utils/userOperation.ts +124 -0
|
@@ -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.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"repository": "https://github.com/ourzora/zora-protocol",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@hey-api/client-fetch": "^0.8.3",
|
|
26
|
-
"@zoralabs/protocol-deployments": "^0.7.
|
|
26
|
+
"@zoralabs/protocol-deployments": "^0.7.6"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"abitype": "^1.0.8",
|
|
30
|
-
"viem": "2.
|
|
30
|
+
"viem": "2.53.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@hey-api/openapi-ts": "^0.64.10",
|
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 =
|
|
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[
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
331
|
+
account,
|
|
202
332
|
};
|
|
203
333
|
|
|
204
334
|
// simulate call
|
|
205
|
-
if (!
|
|
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 =
|
|
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
|
+
}
|