@wayne-zhang/ovault-evm 0.0.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.
Files changed (55) hide show
  1. package/README.md +141 -0
  2. package/artifacts/IVaultComposerSync.sol/IVaultComposerSync.json +1126 -0
  3. package/artifacts/IVaultComposerSyncNative.sol/IVaultComposerSyncNative.json +323 -0
  4. package/artifacts/VaultComposerSync.sol/VaultComposerSync.json +1399 -0
  5. package/artifacts/VaultComposerSyncNative.sol/VaultComposerSyncNative.json +1540 -0
  6. package/dist/contracts/EIP2612.d.ts +14 -0
  7. package/dist/contracts/EIP2612.d.ts.map +1 -0
  8. package/dist/contracts/EIP2612.js +38 -0
  9. package/dist/contracts/ERC20.d.ts +169 -0
  10. package/dist/contracts/ERC20.d.ts.map +1 -0
  11. package/dist/contracts/ERC20.js +222 -0
  12. package/dist/contracts/ERC4626.d.ts +2020 -0
  13. package/dist/contracts/ERC4626.d.ts.map +1 -0
  14. package/dist/contracts/ERC4626.js +2638 -0
  15. package/dist/contracts/OFT.d.ts +1736 -0
  16. package/dist/contracts/OFT.d.ts.map +1 -0
  17. package/dist/contracts/OFT.js +2251 -0
  18. package/dist/contracts/OVaultComposer.d.ts +94 -0
  19. package/dist/contracts/OVaultComposer.d.ts.map +1 -0
  20. package/dist/contracts/OVaultComposer.js +1629 -0
  21. package/dist/contracts/OVaultComposerSync.d.ts +528 -0
  22. package/dist/contracts/OVaultComposerSync.d.ts.map +1 -0
  23. package/dist/contracts/OVaultComposerSync.js +682 -0
  24. package/dist/contracts/OVaultComposerSyncNative.d.ts +560 -0
  25. package/dist/contracts/OVaultComposerSyncNative.d.ts.map +1 -0
  26. package/dist/contracts/OVaultComposerSyncNative.js +723 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +2 -0
  30. package/dist/oVaultSync/index.d.ts +3 -0
  31. package/dist/oVaultSync/index.d.ts.map +1 -0
  32. package/dist/oVaultSync/index.js +2 -0
  33. package/dist/oVaultSync/tracking.d.ts +39 -0
  34. package/dist/oVaultSync/tracking.d.ts.map +1 -0
  35. package/dist/oVaultSync/tracking.js +458 -0
  36. package/dist/oVaultSync/transfer.d.ts +94 -0
  37. package/dist/oVaultSync/transfer.d.ts.map +1 -0
  38. package/dist/oVaultSync/transfer.js +431 -0
  39. package/dist/types.d.ts +149 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +18 -0
  42. package/package.json +87 -0
  43. package/src/contracts/EIP2612.ts +38 -0
  44. package/src/contracts/ERC20.ts +222 -0
  45. package/src/contracts/ERC4626.ts +2638 -0
  46. package/src/contracts/OFT.ts +2251 -0
  47. package/src/contracts/OVaultComposer.ts +1629 -0
  48. package/src/contracts/OVaultComposerSync.ts +682 -0
  49. package/src/index.ts +2 -0
  50. package/src/oVaultSync/index.ts +2 -0
  51. package/src/oVaultSync/tracking.ts +623 -0
  52. package/src/oVaultSync/transfer.ts +615 -0
  53. package/src/types.ts +201 -0
  54. package/test/sdk/tracker.test.ts +18 -0
  55. package/test/sdk/transfer.test.ts +330 -0
@@ -0,0 +1,615 @@
1
+ import {
2
+ createPublicClient,
3
+ encodeAbiParameters,
4
+ erc20Abi,
5
+ hexToBigInt,
6
+ http,
7
+ pad,
8
+ stringToHex,
9
+ } from "viem";
10
+ import { OFTAbi } from "../contracts/OFT";
11
+ import { OVaultComposerSyncAbi } from "../contracts/OVaultComposerSync";
12
+ import { ERC4626_ABI } from "../contracts/ERC4626";
13
+ import { Options } from "@layerzerolabs/lz-v2-utilities";
14
+ import { ERC20Abi } from "../contracts/ERC20";
15
+ import { EIP2612_ABI } from "../contracts/EIP2612";
16
+ import {
17
+ OVaultSyncInputs,
18
+ SendParams,
19
+ SendParamsInput,
20
+ GenerateOVaultSyncInputsProps,
21
+ OVaultSyncOperations,
22
+ } from "../types";
23
+
24
+ const EXTRA_GAS_COMPOSE = 300_000n;
25
+
26
+ export class OVaultSyncMessageBuilder {
27
+ private static getFormattedRefCode(code?: string): `0x${string}` {
28
+ return code
29
+ ? (stringToHex(code, { size: 32 }) as `0x${string}`)
30
+ : ("0x" as `0x${string}`);
31
+ }
32
+
33
+ /**
34
+ * Parse a string amount to BigInt with specified decimals, always truncating (never rounding up)
35
+ * This is critical for LayerZero operations to avoid precision issues
36
+ */
37
+ private static parseUnitsTruncate(value: string, decimals: number): bigint {
38
+ const strValue = String(value);
39
+ const [integerPart, decimalPart = ""] = strValue.split(".");
40
+ const truncatedDecimalPart = decimalPart
41
+ .slice(0, decimals)
42
+ .padEnd(decimals, "0");
43
+ const combined = integerPart + truncatedDecimalPart;
44
+ return BigInt(combined);
45
+ }
46
+
47
+ /**
48
+ * LayerZero uses 6 shared decimals for cross-chain transfers.
49
+ * This function removes dust below the 6 decimal precision that LayerZero won't transfer.
50
+ * For tokens with >6 decimals, this prevents quoteSend failures due to precision loss.
51
+ */
52
+ private static removeLzDust(amount: bigint, decimals: number): bigint {
53
+ if (decimals <= 6) return amount;
54
+ const dust = 10n ** BigInt(decimals - 6);
55
+ return (amount / dust) * dust;
56
+ }
57
+
58
+ static buildComposeArgument(input: SendParamsInput, messageFee: bigint) {
59
+ // For the default OVault, the compose argument is just the hub to dst chain send params
60
+ // However, if you are using a custom OVault, you can have a custom compose argument
61
+ return encodeAbiParameters(
62
+ [
63
+ {
64
+ type: "tuple",
65
+ name: "sendParams",
66
+ components: [
67
+ { name: "dstEid", type: "uint32" },
68
+ { name: "to", type: "bytes32" },
69
+ { name: "amountLD", type: "uint256" },
70
+ { name: "minAmountLD", type: "uint256" },
71
+ { name: "extraOptions", type: "bytes" },
72
+ { name: "composeMsg", type: "bytes" },
73
+ { name: "oftCmd", type: "bytes" },
74
+ ],
75
+ },
76
+ {
77
+ type: "uint256",
78
+ name: "minMsgValue",
79
+ },
80
+ ],
81
+ [this.buildHubToDstChainSendParams(input), messageFee],
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Build the send params for the hub chain to the dst chain.
87
+ *
88
+ * @param input - The input parameters for the OVault.
89
+ * @returns The send params for the hub chain to the dst chain.
90
+ */
91
+ static buildHubToDstChainSendParams(input: SendParamsInput) {
92
+ const {
93
+ srcEid,
94
+ hubEid,
95
+ dstEid,
96
+ dstAddress,
97
+ dstAmount,
98
+ minDstAmount,
99
+ referralCode,
100
+ } = input;
101
+
102
+ const srcIsHubChain = srcEid === hubEid;
103
+
104
+ // This is for a basic OFT send, so the enforced options should be enough
105
+ const options = Options.newOptions();
106
+ return {
107
+ dstEid: dstEid,
108
+ to: pad(dstAddress, { size: 32 }),
109
+ amountLD: dstAmount,
110
+ minAmountLD: minDstAmount,
111
+ extraOptions: options.toHex() as `0x${string}`,
112
+ composeMsg: "0x" as `0x${string}`,
113
+ oftCmd: srcIsHubChain
114
+ ? this.getFormattedRefCode(referralCode)
115
+ : ("0x" as `0x${string}`),
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Quote the amount of shares or assets that will be received from the OVault.
121
+ *
122
+ * @param input - The input parameters for the OVault.
123
+ * @returns The amount of shares or assets that will be received from the OVault.
124
+ */
125
+ static async quoteOVaultOutput(
126
+ input: GenerateOVaultSyncInputsProps,
127
+ ): Promise<{
128
+ dstAmount: bigint;
129
+ minDstAmount: bigint;
130
+ }> {
131
+ const {
132
+ operation,
133
+ vaultAddress,
134
+ hubChain,
135
+ slippage,
136
+ srcEid,
137
+ dstEid,
138
+ hubEid,
139
+ } = input;
140
+
141
+ const client = createPublicClient({
142
+ chain: hubChain,
143
+ transport: http(),
144
+ });
145
+ if (input.amount == null || input.tokenHubDecimals == null) {
146
+ throw new Error("inputs cannot be null");
147
+ }
148
+ let amountInHubDecimals = this.parseUnitsTruncate(
149
+ input.amount,
150
+ input.tokenHubDecimals,
151
+ );
152
+ // Only remove LayerZero dust if this is a cross-chain operation
153
+ // LayerZero is involved if source or destination is not the hub chain
154
+ const isCrossChain = srcEid !== hubEid || dstEid !== hubEid;
155
+ if (isCrossChain) {
156
+ amountInHubDecimals = this.removeLzDust(
157
+ amountInHubDecimals,
158
+ input.tokenHubDecimals,
159
+ );
160
+ }
161
+
162
+ const outputAmount = await client.readContract({
163
+ address: vaultAddress,
164
+ abi: ERC4626_ABI,
165
+ functionName:
166
+ operation === OVaultSyncOperations.DEPOSIT
167
+ ? "previewDeposit"
168
+ : "previewRedeem",
169
+ args: [amountInHubDecimals],
170
+ });
171
+
172
+ if (slippage < 0.001)
173
+ throw new Error("Slippage must be greater or equal to 0.001 (0.1%)");
174
+ if (outputAmount === 0n) throw new Error("Output amount is too small");
175
+
176
+ const slippageAmount =
177
+ (outputAmount * BigInt(Number(slippage.toFixed(3)) * 1000)) / 1000n;
178
+
179
+ return {
180
+ dstAmount: outputAmount,
181
+ minDstAmount: outputAmount - slippageAmount,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Calculate the message fee for the hub chain. This is the fee that it costs to send a message from the hub chain to the dst chain.
187
+ *
188
+ * @param input - The input parameters for the OVault.
189
+ * @param useWalletAddress
190
+ * @returns The message fee for the hub chain.
191
+ */
192
+ static async calculateHubChainFee(
193
+ input: SendParamsInput,
194
+ useWalletAddress = true,
195
+ ) {
196
+ const {
197
+ srcEid,
198
+ operation,
199
+ amount,
200
+ composerAddress,
201
+ hubChain,
202
+ dstEid,
203
+ hubEid,
204
+ oftHubAddress,
205
+ vaultAddress,
206
+ buffer,
207
+ } = input;
208
+
209
+ // If the dst chain is the same as the hub chain, then we don't need to calculate the fee
210
+ // as we are already on the hub chain, so "send" on the OFT will not be called.
211
+ if (dstEid === hubEid) {
212
+ return {
213
+ nativeFee: 0n,
214
+ lzTokenFee: 0n,
215
+ };
216
+ }
217
+
218
+ const client = createPublicClient({
219
+ chain: hubChain,
220
+ transport: http(),
221
+ });
222
+
223
+ const hubSendParams = this.buildHubToDstChainSendParams(input);
224
+
225
+ // Determine the target OFT based on operation:
226
+ // - For deposits: shares are sent via share OFT
227
+ // - For redeems: assets are sent via asset OFT
228
+ const targetOft =
229
+ operation === OVaultSyncOperations.DEPOSIT ? vaultAddress : oftHubAddress;
230
+
231
+ if (srcEid === hubEid) {
232
+ // For same-chain operations, use wallet address. For cross-chain, we could use
233
+ // the OFT address since the wallet may not have the tokens yet.
234
+ if (!useWalletAddress) {
235
+ throw new Error("Wallet address is required");
236
+ }
237
+ const quote = await client.readContract({
238
+ address: composerAddress,
239
+ abi: OVaultComposerSyncAbi,
240
+ functionName: "quoteSend",
241
+ args: [input.walletAddress, targetOft, amount, hubSendParams],
242
+ });
243
+
244
+ return {
245
+ nativeFee: this.increaseByBuffer(quote.nativeFee, buffer),
246
+ lzTokenFee: this.increaseByBuffer(quote.lzTokenFee, buffer),
247
+ };
248
+ }
249
+
250
+ // We check directly on the OFT to get the message fee as the vault address or the share OFT address
251
+ // won't have shares or assets balance as we burn them.
252
+ // This is different to the original LZ implementation, where they use
253
+ // OFT adapter for the shares token in accounting chain (the tokens are locked in that case,
254
+ // not burned). In their case, they always call VaultComposer, for maxDeposit/maxRedeem checks.
255
+ const quote = await client.readContract({
256
+ address: targetOft,
257
+ abi: OFTAbi,
258
+ functionName: "quoteSend",
259
+ args: [hubSendParams as never, false],
260
+ });
261
+
262
+ return {
263
+ nativeFee: this.increaseByBuffer(quote.nativeFee, buffer),
264
+ lzTokenFee: this.increaseByBuffer(quote.lzTokenFee, buffer),
265
+ };
266
+ }
267
+
268
+ static async getMessageFee(sendParams: SendParams, input: SendParamsInput) {
269
+ const {
270
+ operation,
271
+ oftAddress,
272
+ vaultTokenAddress,
273
+ sourceChain,
274
+ srcEid,
275
+ hubEid,
276
+ buffer,
277
+ } = input;
278
+
279
+ const client = createPublicClient({
280
+ chain: sourceChain,
281
+ transport: http(),
282
+ });
283
+
284
+ // If the src chain is the same as the hub chain, then the way to calculate the message fee is the same
285
+ // as calculating the hub chain fee as we are already on the hub chain.
286
+ if (srcEid === hubEid) {
287
+ return this.calculateHubChainFee(input, true);
288
+ }
289
+
290
+ const sendingOftAddress =
291
+ operation === OVaultSyncOperations.DEPOSIT
292
+ ? oftAddress
293
+ : vaultTokenAddress;
294
+
295
+ // If we are not on the hub chain, then we need to call the OFT's quoteSend function to get the message fee.
296
+ const messageFee = await client.readContract({
297
+ address: sendingOftAddress,
298
+ abi: OFTAbi,
299
+ functionName: "quoteSend",
300
+ args: [sendParams as never, false],
301
+ });
302
+
303
+ return {
304
+ nativeFee: this.increaseByBuffer(messageFee.nativeFee, buffer),
305
+ lzTokenFee: this.increaseByBuffer(messageFee.lzTokenFee, buffer),
306
+ };
307
+ }
308
+
309
+ static increaseByBuffer(amount: bigint, buffer: number = 0) {
310
+ if (!buffer || buffer <= 0) {
311
+ return amount;
312
+ }
313
+
314
+ const flatNumber = BigInt((buffer * 100).toFixed(0));
315
+
316
+ const bufferAmount = (amount * flatNumber) / 100n;
317
+ return amount + bufferAmount;
318
+ }
319
+
320
+ static async buildSendParams(input: SendParamsInput) {
321
+ const {
322
+ amount,
323
+ srcEid,
324
+ composerAddress,
325
+ hubEid,
326
+ dstAmount,
327
+ minDstAmount,
328
+ referralCode,
329
+ } = input;
330
+
331
+ const options = Options.newOptions();
332
+
333
+ const srcIsHubChain = srcEid === hubEid;
334
+
335
+ if (srcIsHubChain) {
336
+ return this.buildHubToDstChainSendParams(input);
337
+ }
338
+
339
+ // If the src chain is not the same as the hub chain, then the first message will be an LzCompose
340
+ // so we need to add the executor option
341
+ const hubMessageFee = await this.calculateHubChainFee(
342
+ {
343
+ ...input,
344
+ dstAmount,
345
+ minDstAmount,
346
+ },
347
+ false,
348
+ );
349
+
350
+ const extraGas = input.hubLzComposeGasLimit ?? EXTRA_GAS_COMPOSE;
351
+ options.addExecutorComposeOption(0, extraGas, hubMessageFee.nativeFee);
352
+
353
+ return {
354
+ // If the src chain is not the same as the hub chain, then the first message will be to the hub chain
355
+ // so we need to set the dstEid to the hub chain EID
356
+ dstEid: hubEid,
357
+ to: pad(composerAddress, { size: 32 }),
358
+ amountLD: amount,
359
+ // TODO: find out why no slippage
360
+ minAmountLD: amount,
361
+ extraOptions: options.toHex() as `0x${string}`,
362
+ composeMsg: this.buildComposeArgument(input, hubMessageFee.nativeFee),
363
+ oftCmd: this.getFormattedRefCode(referralCode),
364
+ };
365
+ }
366
+
367
+ static async buildApproval(input: SendParamsInput) {
368
+ const {
369
+ tokenAddress,
370
+ sourceChain,
371
+ composerAddress,
372
+ srcEid,
373
+ hubEid,
374
+ amount,
375
+ walletAddress,
376
+ vaultTokenAddress,
377
+ oftAddress,
378
+ operation,
379
+ supportsEip2612,
380
+ requiresZeroApprovalReset,
381
+ } = input;
382
+
383
+ let spender: `0x${string}` | null = null;
384
+ let approvingTokenAddress: `0x${string}` | null = null;
385
+ const srcIsHubChain = srcEid === hubEid;
386
+
387
+ // 1. We are locking the base token on a spoke chain (UnderlyingOFTAdapter)
388
+ // In this case, we need to approve the OFTAdapter to spend the base token.
389
+ if (!srcIsHubChain && operation == OVaultSyncOperations.DEPOSIT) {
390
+ approvingTokenAddress = tokenAddress;
391
+ spender = oftAddress;
392
+ // 2. We are depositing the base token on the hub chain
393
+ // In this case, we need to approve the VaultComposer to spend the base token (depositAndSend).
394
+ } else if (srcIsHubChain && operation == OVaultSyncOperations.DEPOSIT) {
395
+ approvingTokenAddress = tokenAddress;
396
+ spender = composerAddress;
397
+ // 3. We are requesting a withdrawal on a spoke chain (VaultToken)
398
+ // No need, as it's handled by OFT.send in VaultToken
399
+ } else if (!srcIsHubChain && operation == OVaultSyncOperations.REDEEM) {
400
+ return;
401
+ // 4. We are requesting a withdrawal on the hub chain (Vault)
402
+ // In this case, we need to approve the Composer to spend the shares.
403
+ } else if (srcIsHubChain && operation == OVaultSyncOperations.REDEEM) {
404
+ approvingTokenAddress = vaultTokenAddress;
405
+ spender = composerAddress;
406
+ }
407
+
408
+ // If we couldn't determine spender/token, there's nothing to approve
409
+ if (!approvingTokenAddress || !spender) {
410
+ return;
411
+ }
412
+
413
+ const client = createPublicClient({
414
+ chain: sourceChain,
415
+ transport: http(),
416
+ });
417
+
418
+ const allowance = await client.readContract({
419
+ address: approvingTokenAddress,
420
+ abi: ERC20Abi,
421
+ functionName: "allowance",
422
+ args: [walletAddress, spender],
423
+ });
424
+
425
+ if (allowance >= amount) {
426
+ // We have enough allowance, so we don't need to approve
427
+ return;
428
+ }
429
+
430
+ // Check if the asset supports EIP-2612 permit
431
+ // Only use permit for deposits with the base token (not vault shares)
432
+ const usePermit =
433
+ operation === OVaultSyncOperations.DEPOSIT && supportsEip2612 === true;
434
+
435
+ if (usePermit) {
436
+ try {
437
+ const [nonceResult, tokenNameResult, tokenVersionResult] =
438
+ await client.multicall({
439
+ contracts: [
440
+ {
441
+ address: approvingTokenAddress as `0x${string}`,
442
+ abi: EIP2612_ABI,
443
+ functionName: "nonces",
444
+ args: [walletAddress],
445
+ },
446
+ {
447
+ address: approvingTokenAddress as `0x${string}`,
448
+ abi: erc20Abi,
449
+ functionName: "name",
450
+ },
451
+ {
452
+ address: approvingTokenAddress as `0x${string}`,
453
+ abi: EIP2612_ABI,
454
+ functionName: "version",
455
+ },
456
+ ],
457
+ });
458
+
459
+ if (
460
+ nonceResult.status !== "success" ||
461
+ tokenNameResult.status !== "success"
462
+ ) {
463
+ throw new Error("Failed to fetch permit data");
464
+ }
465
+
466
+ const tokenVersion =
467
+ tokenVersionResult.status !== "success"
468
+ ? "1"
469
+ : (tokenVersionResult.result as string);
470
+
471
+ return {
472
+ tokenAddress: approvingTokenAddress,
473
+ amount,
474
+ spender,
475
+ usePermit: true,
476
+ nonce: nonceResult.result as bigint,
477
+ tokenName: tokenNameResult.result as string,
478
+ tokenVersion,
479
+ };
480
+ } catch (error) {
481
+ // If nonce fetch fails, fall back to regular approval
482
+ console.warn(
483
+ "Failed to fetch nonce for permit, falling back to approval:",
484
+ error,
485
+ );
486
+ }
487
+ }
488
+
489
+ // Some tokens (like USDT) require approval to be set to 0 before changing to a new value
490
+ // If there's an existing non-zero allowance, we need a two-step approval
491
+ const needsResetApproval =
492
+ requiresZeroApprovalReset && allowance > 0n && allowance < amount;
493
+
494
+ return {
495
+ tokenAddress: approvingTokenAddress,
496
+ amount,
497
+ spender,
498
+ usePermit: false,
499
+ needsResetApproval,
500
+ };
501
+ }
502
+
503
+ /**
504
+ * Generate the inputs for the OVault.
505
+ *
506
+ * @param input - The input parameters for the OVault.
507
+ * @returns Inputs to call contracts to perform Deposit or Redeem on the OVault.
508
+ *
509
+ */
510
+ static async generateOVaultInputs(
511
+ input: GenerateOVaultSyncInputsProps,
512
+ ): Promise<OVaultSyncInputs> {
513
+ const {
514
+ srcEid,
515
+ hubEid,
516
+ dstEid,
517
+ operation,
518
+ amount, // amount should be not scaled (string)
519
+ dstAddress,
520
+ walletAddress: refundAddress,
521
+ composerAddress,
522
+ oftAddress, // OFT/OFTAdapter on the source chain
523
+ vaultTokenAddress, // VaultToken address on the source chain
524
+ tokenLocalDecimals,
525
+ } = input;
526
+
527
+ // Calculates the minimum amount based on the slippage
528
+ const outputAmount = await this.quoteOVaultOutput(input);
529
+
530
+ // Parse the amount
531
+ let amountInLocalDecimals = this.parseUnitsTruncate(
532
+ amount,
533
+ tokenLocalDecimals,
534
+ );
535
+
536
+ // Only remove LayerZero dust if this is a cross-chain operation
537
+ // LayerZero is involved if source or destination is not the hub chain
538
+ const isCrossChain = srcEid !== hubEid || dstEid !== hubEid;
539
+ if (isCrossChain) {
540
+ amountInLocalDecimals = this.removeLzDust(
541
+ amountInLocalDecimals,
542
+ tokenLocalDecimals,
543
+ );
544
+ }
545
+
546
+ if (amountInLocalDecimals === 0n) {
547
+ throw new Error("Amount is too small");
548
+ }
549
+
550
+ const fullInputParams = {
551
+ ...input,
552
+ amount: amountInLocalDecimals,
553
+ dstAmount: outputAmount.dstAmount,
554
+ minDstAmount: outputAmount.minDstAmount,
555
+ dstAddress: dstAddress ?? refundAddress,
556
+ };
557
+
558
+ const sendParams = await this.buildSendParams(fullInputParams);
559
+ const messageFee = await this.getMessageFee(sendParams, fullInputParams);
560
+
561
+ const srcIsHubChain = srcEid === hubEid;
562
+
563
+ const approvalData = await this.buildApproval(fullInputParams);
564
+
565
+ if (!srcIsHubChain) {
566
+ // Spoke chain: use 'send' or 'sendWithPermit' for deposits
567
+ const usePermit =
568
+ approvalData?.usePermit && operation === OVaultSyncOperations.DEPOSIT;
569
+
570
+ return {
571
+ messageFee,
572
+ dstAmount: {
573
+ amount: outputAmount.dstAmount,
574
+ minAmount: outputAmount.minDstAmount,
575
+ },
576
+ approval: approvalData,
577
+ txArgs: {
578
+ base: [sendParams, messageFee, refundAddress],
579
+ permitParams: usePermit ? ["deadline", "v", "r", "s"] : null,
580
+ },
581
+ contractFunctionName: usePermit ? "sendWithPermit" : "send",
582
+ contractAddress:
583
+ operation == OVaultSyncOperations.DEPOSIT
584
+ ? oftAddress
585
+ : vaultTokenAddress,
586
+ abi: OFTAbi,
587
+ };
588
+ }
589
+
590
+ // Hub chain: use 'depositAndSend' or 'depositAndSendWithPermit' for deposits
591
+ const usePermit =
592
+ approvalData?.usePermit && operation === OVaultSyncOperations.DEPOSIT;
593
+
594
+ return {
595
+ messageFee,
596
+ dstAmount: {
597
+ amount: outputAmount.dstAmount,
598
+ minAmount: outputAmount.minDstAmount,
599
+ },
600
+ approval: approvalData,
601
+ txArgs: {
602
+ base: [amountInLocalDecimals, sendParams, refundAddress ?? dstAddress],
603
+ permitParams: usePermit ? ["deadline", "v", "r", "s"] : null,
604
+ },
605
+ contractFunctionName:
606
+ operation === OVaultSyncOperations.DEPOSIT
607
+ ? usePermit
608
+ ? "depositAndSendWithPermit"
609
+ : "depositAndSend"
610
+ : "redeemAndSend",
611
+ contractAddress: composerAddress,
612
+ abi: OVaultComposerSyncAbi,
613
+ };
614
+ }
615
+ }