@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
package/src/actions/tradeCoin.ts
CHANGED
|
@@ -2,14 +2,22 @@ import { permit2ABI, permit2Address } from "@zoralabs/protocol-deployments";
|
|
|
2
2
|
import {
|
|
3
3
|
Account,
|
|
4
4
|
Address,
|
|
5
|
+
encodeFunctionData,
|
|
5
6
|
erc20Abi,
|
|
6
|
-
WalletClient,
|
|
7
|
-
maxUint256,
|
|
8
7
|
Hex,
|
|
8
|
+
maxUint256,
|
|
9
|
+
TransactionReceipt,
|
|
10
|
+
WalletClient,
|
|
9
11
|
} from "viem";
|
|
12
|
+
import { BundlerClient, SmartAccount } from "viem/account-abstraction";
|
|
10
13
|
import { base } from "viem/chains";
|
|
11
14
|
import { postQuote, PostQuoteResponse } from "../client";
|
|
15
|
+
import { GenericCall, toUserOperationCalls } from "../utils/calls";
|
|
12
16
|
import { GenericPublicClient } from "../utils/genericPublicClient";
|
|
17
|
+
import {
|
|
18
|
+
prepareUserOperation,
|
|
19
|
+
submitUserOperation,
|
|
20
|
+
} from "../utils/userOperation";
|
|
13
21
|
|
|
14
22
|
type TradeERC20 = {
|
|
15
23
|
type: "erc20";
|
|
@@ -92,6 +100,110 @@ export type TradeParameters = {
|
|
|
92
100
|
permitActiveSeconds?: number;
|
|
93
101
|
};
|
|
94
102
|
|
|
103
|
+
type SignPermitTypedData = (params: {
|
|
104
|
+
domain: { name: string; chainId: number; verifyingContract: Address };
|
|
105
|
+
types: typeof PERMIT_SINGLE_TYPES;
|
|
106
|
+
primaryType: "PermitSingle";
|
|
107
|
+
message: Permit;
|
|
108
|
+
}) => Promise<Hex>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolves the permit2 requirements for a trade quote.
|
|
112
|
+
*
|
|
113
|
+
* For each permit the quote requires, reads the on-chain permit2 nonce and the
|
|
114
|
+
* token's permit2 allowance, signs the permit2 `PermitSingle` typed data with the
|
|
115
|
+
* provided signer, and — when the token's permit2 allowance is insufficient —
|
|
116
|
+
* collects the ERC20 `approve(permit2, max)` call needed before the trade.
|
|
117
|
+
*
|
|
118
|
+
* The approval is returned as a {@link GenericCall} rather than executed, so the
|
|
119
|
+
* caller decides how to run it: `tradeCoin` (EOA) sends it as a prior
|
|
120
|
+
* transaction; `tradeCoinSmartWallet` batches it into the trade's user operation.
|
|
121
|
+
*/
|
|
122
|
+
async function resolveTradePermits({
|
|
123
|
+
quote,
|
|
124
|
+
owner,
|
|
125
|
+
publicClient,
|
|
126
|
+
signTypedData,
|
|
127
|
+
}: {
|
|
128
|
+
quote: PostQuoteResponse;
|
|
129
|
+
owner: Address;
|
|
130
|
+
publicClient: GenericPublicClient;
|
|
131
|
+
signTypedData: SignPermitTypedData;
|
|
132
|
+
}): Promise<{
|
|
133
|
+
signatures: SignatureWithPermit<PermitStringAmounts>[];
|
|
134
|
+
approvalCalls: GenericCall[];
|
|
135
|
+
}> {
|
|
136
|
+
const signatures: SignatureWithPermit<PermitStringAmounts>[] = [];
|
|
137
|
+
const approvalCalls: GenericCall[] = [];
|
|
138
|
+
|
|
139
|
+
if (!quote.permits) {
|
|
140
|
+
return { signatures, approvalCalls };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const permit of quote.permits) {
|
|
144
|
+
// permit2 allowance returns [amount, expiration, nonce]
|
|
145
|
+
const [, , nonce] = await publicClient.readContract({
|
|
146
|
+
abi: permit2ABI,
|
|
147
|
+
address: permit2Address[base.id],
|
|
148
|
+
functionName: "allowance",
|
|
149
|
+
args: [
|
|
150
|
+
owner,
|
|
151
|
+
permit.permit.details.token as Address,
|
|
152
|
+
permit.permit.spender as Address,
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const permitToken = permit.permit.details.token as Address;
|
|
157
|
+
const allowance = await publicClient.readContract({
|
|
158
|
+
abi: erc20Abi,
|
|
159
|
+
address: permitToken,
|
|
160
|
+
functionName: "allowance",
|
|
161
|
+
args: [owner, permit2Address[base.id]],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (allowance < BigInt(permit.permit.details.amount)) {
|
|
165
|
+
approvalCalls.push({
|
|
166
|
+
to: permitToken,
|
|
167
|
+
data: encodeFunctionData({
|
|
168
|
+
abi: erc20Abi,
|
|
169
|
+
functionName: "approve",
|
|
170
|
+
args: [permit2Address[base.id], maxUint256],
|
|
171
|
+
}),
|
|
172
|
+
value: 0n,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const message: Permit = {
|
|
177
|
+
details: {
|
|
178
|
+
token: permit.permit.details.token as Address,
|
|
179
|
+
amount: BigInt(permit.permit.details.amount!),
|
|
180
|
+
expiration: Number(permit.permit.details.expiration!),
|
|
181
|
+
nonce,
|
|
182
|
+
},
|
|
183
|
+
spender: permit.permit.spender as Address,
|
|
184
|
+
sigDeadline: BigInt(permit.permit.sigDeadline!),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const signature = await signTypedData({
|
|
188
|
+
domain: {
|
|
189
|
+
name: "Permit2",
|
|
190
|
+
chainId: base.id,
|
|
191
|
+
verifyingContract: permit2Address[base.id],
|
|
192
|
+
},
|
|
193
|
+
primaryType: "PermitSingle",
|
|
194
|
+
types: PERMIT_SINGLE_TYPES,
|
|
195
|
+
message,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
signatures.push({
|
|
199
|
+
signature,
|
|
200
|
+
permit: convertBigIntToString(message),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { signatures, approvalCalls };
|
|
205
|
+
}
|
|
206
|
+
|
|
95
207
|
export async function tradeCoin({
|
|
96
208
|
tradeParameters,
|
|
97
209
|
walletClient,
|
|
@@ -104,7 +216,7 @@ export async function tradeCoin({
|
|
|
104
216
|
account?: Account | Address;
|
|
105
217
|
publicClient: GenericPublicClient;
|
|
106
218
|
validateTransaction?: boolean;
|
|
107
|
-
}) {
|
|
219
|
+
}): Promise<TransactionReceipt> {
|
|
108
220
|
const quote = await createTradeCall(tradeParameters);
|
|
109
221
|
|
|
110
222
|
if (!account) {
|
|
@@ -113,77 +225,33 @@ export async function tradeCoin({
|
|
|
113
225
|
if (!account) {
|
|
114
226
|
throw new Error("Account is required");
|
|
115
227
|
}
|
|
228
|
+
const resolvedAccount = account;
|
|
229
|
+
const owner =
|
|
230
|
+
typeof resolvedAccount === "string"
|
|
231
|
+
? resolvedAccount
|
|
232
|
+
: resolvedAccount.address;
|
|
116
233
|
|
|
117
234
|
// Set default recipient to wallet sender address if not provided
|
|
118
235
|
if (!tradeParameters.recipient) {
|
|
119
|
-
tradeParameters.recipient =
|
|
120
|
-
typeof account === "string" ? account : account.address;
|
|
236
|
+
tradeParameters.recipient = owner;
|
|
121
237
|
}
|
|
122
238
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const allowance = await publicClient.readContract({
|
|
140
|
-
abi: erc20Abi,
|
|
141
|
-
address: permitToken,
|
|
142
|
-
functionName: "allowance",
|
|
143
|
-
args: [
|
|
144
|
-
typeof account === "string" ? account : account.address,
|
|
145
|
-
permit2Address[base.id],
|
|
146
|
-
],
|
|
147
|
-
});
|
|
148
|
-
if (allowance < BigInt(permit.permit.details.amount)) {
|
|
149
|
-
const approvalTx = await walletClient.writeContract({
|
|
150
|
-
abi: erc20Abi,
|
|
151
|
-
address: permitToken,
|
|
152
|
-
functionName: "approve",
|
|
153
|
-
chain: base,
|
|
154
|
-
args: [permit2Address[base.id], maxUint256],
|
|
155
|
-
account,
|
|
156
|
-
});
|
|
157
|
-
await publicClient.waitForTransactionReceipt({
|
|
158
|
-
hash: approvalTx,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
const message = {
|
|
162
|
-
details: {
|
|
163
|
-
token: permit.permit.details.token as Address,
|
|
164
|
-
amount: BigInt(permit.permit.details.amount!),
|
|
165
|
-
expiration: Number(permit.permit.details.expiration!),
|
|
166
|
-
nonce: nonce,
|
|
167
|
-
},
|
|
168
|
-
spender: permit.permit.spender as Address,
|
|
169
|
-
sigDeadline: BigInt(permit.permit.sigDeadline!),
|
|
170
|
-
};
|
|
171
|
-
const signature = await walletClient.signTypedData({
|
|
172
|
-
domain: {
|
|
173
|
-
name: "Permit2",
|
|
174
|
-
chainId: base.id,
|
|
175
|
-
verifyingContract: permit2Address[base.id],
|
|
176
|
-
},
|
|
177
|
-
primaryType: "PermitSingle",
|
|
178
|
-
types: PERMIT_SINGLE_TYPES,
|
|
179
|
-
message,
|
|
180
|
-
account,
|
|
181
|
-
});
|
|
182
|
-
signatures.push({
|
|
183
|
-
signature,
|
|
184
|
-
permit: convertBigIntToString(message),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
239
|
+
const { signatures, approvalCalls } = await resolveTradePermits({
|
|
240
|
+
quote,
|
|
241
|
+
owner,
|
|
242
|
+
publicClient,
|
|
243
|
+
signTypedData: (typedData) =>
|
|
244
|
+
walletClient.signTypedData({ ...typedData, account: resolvedAccount }),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// EOA path: execute each required permit2 approval as its own transaction
|
|
248
|
+
for (const approvalCall of approvalCalls) {
|
|
249
|
+
const approvalTx = await walletClient.sendTransaction({
|
|
250
|
+
...approvalCall,
|
|
251
|
+
account: resolvedAccount,
|
|
252
|
+
chain: base,
|
|
253
|
+
});
|
|
254
|
+
await publicClient.waitForTransactionReceipt({ hash: approvalTx });
|
|
187
255
|
}
|
|
188
256
|
|
|
189
257
|
const newQuote = await createTradeCall({
|
|
@@ -196,7 +264,7 @@ export async function tradeCoin({
|
|
|
196
264
|
data: newQuote.call.data as Hex,
|
|
197
265
|
value: BigInt(newQuote.call.value),
|
|
198
266
|
chain: base,
|
|
199
|
-
account,
|
|
267
|
+
account: resolvedAccount,
|
|
200
268
|
};
|
|
201
269
|
|
|
202
270
|
// simulate call
|
|
@@ -222,15 +290,112 @@ export async function tradeCoin({
|
|
|
222
290
|
return receipt;
|
|
223
291
|
}
|
|
224
292
|
|
|
225
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Executes a trade from the caller's smart wallet via a user operation.
|
|
295
|
+
*
|
|
296
|
+
* Mirrors {@link tradeCoin} but routes through a bundler client: the smart
|
|
297
|
+
* account is both the token holder and the permit2 signer (ERC-1271), and any
|
|
298
|
+
* required permit2 approval is batched into the same user operation as the trade
|
|
299
|
+
* (rather than sent as a prior transaction). Returns the settled transaction
|
|
300
|
+
* receipt.
|
|
301
|
+
*/
|
|
302
|
+
export async function tradeCoinSmartWallet({
|
|
303
|
+
tradeParameters,
|
|
304
|
+
bundlerClient,
|
|
305
|
+
account,
|
|
306
|
+
publicClient,
|
|
307
|
+
}: {
|
|
308
|
+
tradeParameters: TradeParameters;
|
|
309
|
+
bundlerClient: BundlerClient;
|
|
310
|
+
account?: SmartAccount;
|
|
311
|
+
publicClient: GenericPublicClient;
|
|
312
|
+
}) {
|
|
313
|
+
const resolvedAccount = account ?? bundlerClient.account;
|
|
314
|
+
if (!resolvedAccount) {
|
|
315
|
+
throw new Error("Account is required");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const owner = resolvedAccount.address;
|
|
319
|
+
|
|
320
|
+
// The smart wallet is both the sender (token holder) and the permit signer.
|
|
321
|
+
const params: TradeParameters = {
|
|
322
|
+
...tradeParameters,
|
|
323
|
+
sender: owner,
|
|
324
|
+
recipient: tradeParameters.recipient ?? owner,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const quote = await createTradeCall(params);
|
|
328
|
+
|
|
329
|
+
const { signatures, approvalCalls } = await resolveTradePermits({
|
|
330
|
+
quote,
|
|
331
|
+
owner,
|
|
332
|
+
publicClient,
|
|
333
|
+
signTypedData: (typedData) => resolvedAccount.signTypedData(typedData),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const newQuote = await createTradeCall({
|
|
337
|
+
...params,
|
|
338
|
+
signatures,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const tradeCall: GenericCall = {
|
|
342
|
+
to: newQuote.call.target as Address,
|
|
343
|
+
data: newQuote.call.data as Hex,
|
|
344
|
+
value: BigInt(newQuote.call.value),
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Batch any required permit2 approvals + the trade into one user operation
|
|
348
|
+
const calls = toUserOperationCalls([...approvalCalls, tradeCall]);
|
|
349
|
+
|
|
350
|
+
const userOp = await prepareUserOperation({
|
|
351
|
+
bundlerClient,
|
|
352
|
+
account: resolvedAccount,
|
|
353
|
+
calls,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const userOpReceipt = await submitUserOperation({
|
|
357
|
+
bundlerClient,
|
|
358
|
+
account: resolvedAccount,
|
|
359
|
+
userOperation: userOp,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (!userOpReceipt.success) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`User operation reverted${userOpReceipt.reason ? `: ${userOpReceipt.reason}` : ""}`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return userOpReceipt.receipt;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Validates the parameters for a trade.
|
|
373
|
+
*
|
|
374
|
+
* Asserts slippage is within bounds and a non-zero input amount is provided.
|
|
375
|
+
* Shared by the quote builder (`createTradeCall`) and the user-operation path so
|
|
376
|
+
* both validate identically before any network request is made.
|
|
377
|
+
*/
|
|
378
|
+
export function validateTradeParameters(
|
|
226
379
|
tradeParameters: TradeParameters,
|
|
227
|
-
):
|
|
380
|
+
): void {
|
|
228
381
|
if (tradeParameters.slippage && tradeParameters.slippage > 1) {
|
|
229
382
|
throw new Error("Slippage must be less than 1, max 0.99");
|
|
230
383
|
}
|
|
231
384
|
if (tradeParameters.amountIn === BigInt(0)) {
|
|
232
385
|
throw new Error("Amount in must be greater than 0");
|
|
233
386
|
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function createTradeCall(
|
|
390
|
+
tradeParameters: TradeParameters,
|
|
391
|
+
): Promise<PostQuoteResponse> {
|
|
392
|
+
return createQuote(tradeParameters);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function createQuote(
|
|
396
|
+
tradeParameters: TradeParameters,
|
|
397
|
+
): Promise<PostQuoteResponse> {
|
|
398
|
+
validateTradeParameters(tradeParameters);
|
|
234
399
|
|
|
235
400
|
const quote = await postQuote({
|
|
236
401
|
body: {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { coinABI } from "@zoralabs/protocol-deployments";
|
|
2
|
-
import { validateClientNetwork } from "../utils/validateClientNetwork";
|
|
3
2
|
import {
|
|
4
3
|
Account,
|
|
5
4
|
Address,
|
|
@@ -7,21 +6,39 @@ import {
|
|
|
7
6
|
SimulateContractParameters,
|
|
8
7
|
WalletClient,
|
|
9
8
|
} from "viem";
|
|
10
|
-
import {
|
|
9
|
+
import { BundlerClient, SmartAccount } from "viem/account-abstraction";
|
|
11
10
|
import { getAttribution } from "../utils/attribution";
|
|
11
|
+
import { toGenericCall, toUserOperationCalls } from "../utils/calls";
|
|
12
|
+
import { GenericPublicClient } from "../utils/genericPublicClient";
|
|
13
|
+
import {
|
|
14
|
+
prepareUserOperation,
|
|
15
|
+
submitUserOperation,
|
|
16
|
+
} from "../utils/userOperation";
|
|
17
|
+
import { validateClientNetwork } from "../utils/validateClientNetwork";
|
|
12
18
|
|
|
13
19
|
export type UpdateCoinURIArgs = {
|
|
14
20
|
coin: Address;
|
|
15
21
|
newURI: string;
|
|
16
22
|
};
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Validates the arguments for updating a coin's URI.
|
|
26
|
+
*
|
|
27
|
+
* Asserts the new URI is an `ipfs://` URI. Shared by the contract-call builder
|
|
28
|
+
* (`updateCoinURICall`) and the user-operation path so both validate identically.
|
|
29
|
+
*/
|
|
30
|
+
export function validateUpdateCoinURI({ newURI }: UpdateCoinURIArgs): void {
|
|
22
31
|
if (!newURI.startsWith("ipfs://")) {
|
|
23
32
|
throw new Error("URI needs to be an ipfs:// prefix uri");
|
|
24
33
|
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function updateCoinURICall(
|
|
37
|
+
args: UpdateCoinURIArgs,
|
|
38
|
+
): SimulateContractParameters {
|
|
39
|
+
validateUpdateCoinURI(args);
|
|
40
|
+
|
|
41
|
+
const { coin, newURI } = args;
|
|
25
42
|
|
|
26
43
|
return {
|
|
27
44
|
abi: coinABI,
|
|
@@ -39,13 +56,18 @@ export async function updateCoinURI(
|
|
|
39
56
|
account?: Account | Address,
|
|
40
57
|
) {
|
|
41
58
|
validateClientNetwork(publicClient);
|
|
59
|
+
|
|
42
60
|
const call = updateCoinURICall(args);
|
|
61
|
+
|
|
43
62
|
const { request } = await publicClient.simulateContract({
|
|
44
63
|
...call,
|
|
45
64
|
account: account ?? walletClient.account,
|
|
46
65
|
});
|
|
66
|
+
|
|
47
67
|
const hash = await walletClient.writeContract(request);
|
|
68
|
+
|
|
48
69
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
70
|
+
|
|
49
71
|
const eventLogs = parseEventLogs({ abi: coinABI, logs: receipt.logs });
|
|
50
72
|
const uriUpdated = eventLogs.find(
|
|
51
73
|
(log) => log.eventName === "ContractURIUpdated",
|
|
@@ -53,3 +75,59 @@ export async function updateCoinURI(
|
|
|
53
75
|
|
|
54
76
|
return { hash, receipt, uriUpdated };
|
|
55
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Updates a coin's URI from the caller's smart wallet via a user operation.
|
|
81
|
+
*
|
|
82
|
+
* Builds the `setContractURI` call, submits it through the bundler client (which
|
|
83
|
+
* wraps it in the smart wallet's `execute`), and parses the result. Mirrors
|
|
84
|
+
* {@link updateCoinURI}'s return shape (`hash` is the settled transaction hash,
|
|
85
|
+
* `receipt` the underlying transaction receipt).
|
|
86
|
+
*/
|
|
87
|
+
export async function updateCoinURISmartWallet(
|
|
88
|
+
args: UpdateCoinURIArgs,
|
|
89
|
+
bundlerClient: BundlerClient,
|
|
90
|
+
publicClient: GenericPublicClient,
|
|
91
|
+
account?: SmartAccount,
|
|
92
|
+
) {
|
|
93
|
+
const resolvedAccount = account ?? bundlerClient.account;
|
|
94
|
+
if (!resolvedAccount) {
|
|
95
|
+
throw new Error("Account is required");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
validateClientNetwork(publicClient);
|
|
99
|
+
|
|
100
|
+
// updateCoinURICall validates the args and assembles the contract call
|
|
101
|
+
const call = updateCoinURICall(args);
|
|
102
|
+
|
|
103
|
+
const calls = toUserOperationCalls([toGenericCall(call)]);
|
|
104
|
+
|
|
105
|
+
const userOp = await prepareUserOperation({
|
|
106
|
+
bundlerClient,
|
|
107
|
+
account: resolvedAccount,
|
|
108
|
+
calls,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const userOpReceipt = await submitUserOperation({
|
|
112
|
+
bundlerClient,
|
|
113
|
+
account: resolvedAccount,
|
|
114
|
+
userOperation: userOp,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!userOpReceipt.success) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`User operation reverted${userOpReceipt.reason ? `: ${userOpReceipt.reason}` : ""}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const eventLogs = parseEventLogs({ abi: coinABI, logs: userOpReceipt.logs });
|
|
124
|
+
const uriUpdated = eventLogs.find(
|
|
125
|
+
(log) => log.eventName === "ContractURIUpdated",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
hash: userOpReceipt.receipt.transactionHash,
|
|
130
|
+
receipt: userOpReceipt.receipt,
|
|
131
|
+
uriUpdated,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
import { coinABI } from "@zoralabs/protocol-deployments";
|
|
2
|
-
import { validateClientNetwork } from "../utils/validateClientNetwork";
|
|
3
2
|
import {
|
|
4
3
|
Account,
|
|
5
4
|
Address,
|
|
5
|
+
isAddress,
|
|
6
6
|
parseEventLogs,
|
|
7
7
|
SimulateContractParameters,
|
|
8
8
|
WalletClient,
|
|
9
9
|
} from "viem";
|
|
10
|
-
import {
|
|
10
|
+
import { BundlerClient, SmartAccount } from "viem/account-abstraction";
|
|
11
11
|
import { getAttribution } from "../utils/attribution";
|
|
12
|
+
import { toGenericCall, toUserOperationCalls } from "../utils/calls";
|
|
13
|
+
import { GenericPublicClient } from "../utils/genericPublicClient";
|
|
14
|
+
import {
|
|
15
|
+
prepareUserOperation,
|
|
16
|
+
submitUserOperation,
|
|
17
|
+
} from "../utils/userOperation";
|
|
18
|
+
import { validateClientNetwork } from "../utils/validateClientNetwork";
|
|
12
19
|
|
|
13
20
|
export type UpdatePayoutRecipientArgs = {
|
|
14
21
|
coin: Address;
|
|
15
22
|
newPayoutRecipient: string;
|
|
16
23
|
};
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Validates the arguments for updating a coin's payout recipient.
|
|
27
|
+
*
|
|
28
|
+
* Asserts the new payout recipient is a valid address. Shared by the
|
|
29
|
+
* contract-call builder (`updatePayoutRecipientCall`) and the user-operation path
|
|
30
|
+
* so both validate identically.
|
|
31
|
+
*/
|
|
32
|
+
export function validateUpdatePayoutRecipient({
|
|
19
33
|
newPayoutRecipient,
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
}: UpdatePayoutRecipientArgs): void {
|
|
35
|
+
if (!isAddress(newPayoutRecipient)) {
|
|
36
|
+
throw new Error("Payout recipient must be a valid address");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function updatePayoutRecipientCall(
|
|
41
|
+
args: UpdatePayoutRecipientArgs,
|
|
42
|
+
): SimulateContractParameters {
|
|
43
|
+
validateUpdatePayoutRecipient(args);
|
|
44
|
+
|
|
45
|
+
const { coin, newPayoutRecipient } = args;
|
|
46
|
+
|
|
22
47
|
return {
|
|
23
48
|
abi: coinABI,
|
|
24
49
|
address: coin,
|
|
@@ -35,13 +60,18 @@ export async function updatePayoutRecipient(
|
|
|
35
60
|
account?: Account | Address,
|
|
36
61
|
) {
|
|
37
62
|
validateClientNetwork(publicClient);
|
|
63
|
+
|
|
38
64
|
const call = updatePayoutRecipientCall(args);
|
|
65
|
+
|
|
39
66
|
const { request } = await publicClient.simulateContract({
|
|
40
67
|
...call,
|
|
41
68
|
account: account ?? walletClient.account!,
|
|
42
69
|
});
|
|
70
|
+
|
|
43
71
|
const hash = await walletClient.writeContract(request);
|
|
72
|
+
|
|
44
73
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
74
|
+
|
|
45
75
|
const eventLogs = parseEventLogs({ abi: coinABI, logs: receipt.logs });
|
|
46
76
|
const payoutRecipientUpdated = eventLogs.find(
|
|
47
77
|
(log) => log.eventName === "CoinPayoutRecipientUpdated",
|
|
@@ -49,3 +79,60 @@ export async function updatePayoutRecipient(
|
|
|
49
79
|
|
|
50
80
|
return { hash, receipt, payoutRecipientUpdated };
|
|
51
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Updates a coin's payout recipient from the caller's smart wallet via a user
|
|
85
|
+
* operation.
|
|
86
|
+
*
|
|
87
|
+
* Builds the `setPayoutRecipient` call, submits it through the bundler client
|
|
88
|
+
* (which wraps it in the smart wallet's `execute`), and parses the result.
|
|
89
|
+
* Mirrors {@link updatePayoutRecipient}'s return shape (`hash` is the settled
|
|
90
|
+
* transaction hash, `receipt` the underlying transaction receipt).
|
|
91
|
+
*/
|
|
92
|
+
export async function updatePayoutRecipientSmartWallet(
|
|
93
|
+
args: UpdatePayoutRecipientArgs,
|
|
94
|
+
bundlerClient: BundlerClient,
|
|
95
|
+
publicClient: GenericPublicClient,
|
|
96
|
+
account?: SmartAccount,
|
|
97
|
+
) {
|
|
98
|
+
const resolvedAccount = account ?? bundlerClient.account;
|
|
99
|
+
if (!resolvedAccount) {
|
|
100
|
+
throw new Error("Account is required");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
validateClientNetwork(publicClient);
|
|
104
|
+
|
|
105
|
+
// updatePayoutRecipientCall validates the args and assembles the contract call
|
|
106
|
+
const call = updatePayoutRecipientCall(args);
|
|
107
|
+
|
|
108
|
+
const calls = toUserOperationCalls([toGenericCall(call)]);
|
|
109
|
+
|
|
110
|
+
const userOp = await prepareUserOperation({
|
|
111
|
+
bundlerClient,
|
|
112
|
+
account: resolvedAccount,
|
|
113
|
+
calls,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const userOpReceipt = await submitUserOperation({
|
|
117
|
+
bundlerClient,
|
|
118
|
+
account: resolvedAccount,
|
|
119
|
+
userOperation: userOp,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!userOpReceipt.success) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`User operation reverted${userOpReceipt.reason ? `: ${userOpReceipt.reason}` : ""}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const eventLogs = parseEventLogs({ abi: coinABI, logs: userOpReceipt.logs });
|
|
129
|
+
const payoutRecipientUpdated = eventLogs.find(
|
|
130
|
+
(log) => log.eventName === "CoinPayoutRecipientUpdated",
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
hash: userOpReceipt.receipt.transactionHash,
|
|
135
|
+
receipt: userOpReceipt.receipt,
|
|
136
|
+
payoutRecipientUpdated,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { client } from "../client/client.gen";
|
|
3
|
+
import { apiUrl } from "./api-raw";
|
|
4
|
+
|
|
5
|
+
const mockBaseUrl = (baseUrl: string | undefined) =>
|
|
6
|
+
vi
|
|
7
|
+
.spyOn(client, "getConfig")
|
|
8
|
+
.mockReturnValue({ baseUrl } as ReturnType<typeof client.getConfig>);
|
|
9
|
+
|
|
10
|
+
describe("apiUrl", () => {
|
|
11
|
+
const expected = "https://api.example.com/some/path";
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("joins base without trailing slash and path with leading slash", () => {
|
|
18
|
+
mockBaseUrl("https://api.example.com");
|
|
19
|
+
expect(apiUrl("/some/path")).toBe(expected);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("joins base without trailing slash and path without leading slash", () => {
|
|
23
|
+
mockBaseUrl("https://api.example.com");
|
|
24
|
+
expect(apiUrl("some/path")).toBe(expected);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("joins base with trailing slash and path with leading slash", () => {
|
|
28
|
+
mockBaseUrl("https://api.example.com/");
|
|
29
|
+
expect(apiUrl("/some/path")).toBe(expected);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("joins base with trailing slash and path without leading slash", () => {
|
|
33
|
+
mockBaseUrl("https://api.example.com/");
|
|
34
|
+
expect(apiUrl("some/path")).toBe(expected);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("collapses multiple trailing slashes on the base", () => {
|
|
38
|
+
mockBaseUrl("https://api.example.com///");
|
|
39
|
+
expect(apiUrl("some/path")).toBe(expected);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("collapses multiple leading slashes on the path", () => {
|
|
43
|
+
mockBaseUrl("https://api.example.com");
|
|
44
|
+
expect(apiUrl("///some/path")).toBe(expected);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("preserves the protocol's double slashes", () => {
|
|
48
|
+
mockBaseUrl("https://api.example.com/");
|
|
49
|
+
expect(apiUrl("/some/path")).toContain("https://");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("handles an undefined base url", () => {
|
|
53
|
+
mockBaseUrl(undefined);
|
|
54
|
+
expect(apiUrl("/some/path")).toBe("/some/path");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("handles an empty path", () => {
|
|
58
|
+
mockBaseUrl("https://api.example.com");
|
|
59
|
+
expect(apiUrl("")).toBe("https://api.example.com/");
|
|
60
|
+
});
|
|
61
|
+
});
|