Skip to content

Commit c9b1d0a

Browse files
Chore/oft approve e2e (#884)
1 parent 7516e37 commit c9b1d0a

File tree

3 files changed

+182
-7
lines changed

3 files changed

+182
-7
lines changed

sdk/src/gateway/abi.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,19 @@ export const layerZeroOftAbi = [
243243
],
244244
stateMutability: 'payable',
245245
},
246+
{
247+
inputs: [],
248+
name: 'approvalRequired',
249+
outputs: [
250+
{
251+
internalType: 'bool',
252+
name: '',
253+
type: 'bool',
254+
},
255+
],
256+
stateMutability: 'pure',
257+
type: 'function',
258+
},
246259
] as const;
247260

248261
export const quoterV2Abi = [

sdk/src/gateway/layerzero.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -758,13 +758,17 @@ export class LayerZeroGatewayClient extends GatewayApiClient {
758758
};
759759

760760
try {
761-
// ETH -> %any_chain%
762-
if (data.sourceEid === 30101) {
763-
const wbtcMainnetAddress = getTokenAddress(mainnet.id, 'wbtc');
761+
const approvalRequired = await publicClient.readContract({
762+
account: walletClient.account,
763+
address: oftAddress,
764+
abi: layerZeroOftAbi,
765+
functionName: 'approvalRequired',
766+
});
764767

768+
if (approvalRequired) {
765769
const allowance = await publicClient.readContract({
766770
account: walletClient.account,
767-
address: wbtcMainnetAddress,
771+
address: oftAddress,
768772
abi: erc20Abi,
769773
functionName: 'allowance',
770774
args: [params.fromUserAddress as Address, oftAddress],
@@ -773,7 +777,7 @@ export class LayerZeroGatewayClient extends GatewayApiClient {
773777
if (allowance < sendParam.amountLD) {
774778
const { request } = await publicClient.simulateContract({
775779
account: walletClient.account,
776-
address: wbtcMainnetAddress,
780+
address: oftAddress,
777781
abi: erc20Abi,
778782
functionName: 'approve',
779783
args: [oftAddress, maxUint256],

sdk/test/layerzero.test.ts

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import {
77
erc20Abi,
88
http,
99
PublicClient,
10+
SimulateContractReturnType,
11+
TransactionReceipt,
1012
Transport,
1113
zeroAddress,
14+
zeroHash,
1215
} from 'viem';
1316
import {
1417
bob,
@@ -25,7 +28,13 @@ import {
2528
optimism,
2629
sei,
2730
} from 'viem/chains';
28-
import { BitcoinSigner, GetQuoteParams, LayerZeroMessageWallet } from '../src/gateway/types';
31+
import {
32+
BitcoinSigner,
33+
ExecuteQuoteParams,
34+
GatewayOrderType,
35+
GetQuoteParams,
36+
LayerZeroMessageWallet,
37+
} from '../src/gateway/types';
2938
import * as btc from '@scure/btc-signer';
3039
import { base64 } from '@scure/base';
3140
import { mnemonicToSeedSync } from 'bip39';
@@ -566,6 +575,155 @@ describe('LayerZero Tests', () => {
566575
).rejects.toThrow('WBTC OFT not found for chain: 40184');
567576
});
568577
});
578+
579+
it('should execute approve if approval required', async () => {
580+
const client = new LayerZeroGatewayClient();
581+
582+
const mockQuote: ExecuteQuoteParams = {
583+
type: GatewayOrderType.CrossChainSwap,
584+
finalOutputSats: 100000,
585+
finalFeeSats: 0,
586+
params: {
587+
fromChain: 'ethereum',
588+
fromToken: '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c',
589+
toChain: 'bob',
590+
toToken: '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c',
591+
fromUserAddress: '0x5AC6f7ee4D571872333845e8Abc0bDef200B4Df6',
592+
toUserAddress: '0x5AC6f7ee4D571872333845e8Abc0bDef200B4Df6',
593+
amount: '100000',
594+
feeRate: 1,
595+
},
596+
data: {
597+
oftAddress: '0x0555e30da8f98308edb960aa94c0db47230d2b9c',
598+
destinationEid: 30279,
599+
sourceEid: 30101,
600+
feeBreakdown: {
601+
nativeFee: BigInt('245222054031895'),
602+
lzTokenFee: BigInt('0'),
603+
},
604+
},
605+
};
606+
607+
const publicClient = createPublicClient({
608+
chain: mainnet,
609+
transport: http(),
610+
});
611+
612+
const publicClientReadContractSpy = vi
613+
.spyOn(publicClient, 'readContract')
614+
.mockImplementationOnce(() => Promise.resolve(true))
615+
.mockImplementationOnce(() => Promise.resolve(10n));
616+
617+
const publicClientWaitForTransactionReceiptSpy = vi
618+
.spyOn(publicClient, 'waitForTransactionReceipt')
619+
.mockImplementation(() => {
620+
const txReceipt: TransactionReceipt = {} as TransactionReceipt;
621+
return Promise.resolve(txReceipt);
622+
});
623+
624+
const publicClientSimulateContractSpy = vi.spyOn(publicClient, 'simulateContract').mockImplementation(() => {
625+
const simulateContractReturnType: Awaited<ReturnType<typeof publicClient.simulateContract>> = {} as Awaited<
626+
ReturnType<typeof publicClient.simulateContract>
627+
>;
628+
return Promise.resolve(simulateContractReturnType);
629+
});
630+
631+
const walletClient = createWalletClient({
632+
chain: mainnet,
633+
transport: http(),
634+
account: privateKeyToAccount(process.env.PRIVATE_KEY as Hex),
635+
});
636+
637+
const walletClientWriteContractSpy = vi.spyOn(walletClient, 'writeContract').mockImplementation(() => {
638+
const writeContractReturnType = {} as Awaited<ReturnType<typeof walletClient.writeContract>>;
639+
return Promise.resolve(writeContractReturnType);
640+
});
641+
642+
const txHash = await client.executeQuote({
643+
quote: mockQuote,
644+
walletClient,
645+
publicClient: publicClient as PublicClient<Transport>,
646+
});
647+
648+
expect(txHash).toBeDefined();
649+
expect(publicClientReadContractSpy).toHaveBeenCalledTimes(2);
650+
expect(publicClientWaitForTransactionReceiptSpy).toHaveBeenCalledTimes(2);
651+
expect(publicClientSimulateContractSpy).toHaveBeenCalledTimes(2);
652+
expect(walletClientWriteContractSpy).toHaveBeenCalledTimes(2);
653+
}, 120000);
654+
655+
it('should skip approve step', async () => {
656+
const client = new LayerZeroGatewayClient();
657+
658+
const mockQuote: ExecuteQuoteParams = {
659+
type: GatewayOrderType.CrossChainSwap,
660+
finalOutputSats: 100000,
661+
finalFeeSats: 0,
662+
params: {
663+
fromChain: 'bsc',
664+
fromToken: '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c',
665+
toChain: 'bob',
666+
toToken: '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c',
667+
fromUserAddress: '0x5AC6f7ee4D571872333845e8Abc0bDef200B4Df6',
668+
toUserAddress: '0x5AC6f7ee4D571872333845e8Abc0bDef200B4Df6',
669+
amount: '100000',
670+
feeRate: 1,
671+
},
672+
data: {
673+
oftAddress: '0x0555e30da8f98308edb960aa94c0db47230d2b9c',
674+
destinationEid: 30279,
675+
sourceEid: 30102,
676+
feeBreakdown: {
677+
nativeFee: BigInt('245222054031895'),
678+
lzTokenFee: BigInt('0'),
679+
},
680+
},
681+
};
682+
683+
const publicClient = createPublicClient({
684+
chain: bsc,
685+
transport: http(),
686+
});
687+
688+
const publicClientReadContractSpy = vi.spyOn(publicClient, 'readContract');
689+
690+
const publicClientWaitForTransactionReceiptSpy = vi
691+
.spyOn(publicClient, 'waitForTransactionReceipt')
692+
.mockImplementation(() => {
693+
const txReceipt: TransactionReceipt = {} as TransactionReceipt;
694+
return Promise.resolve(txReceipt);
695+
});
696+
697+
const publicClientSimulateContractSpy = vi.spyOn(publicClient, 'simulateContract').mockImplementation(() => {
698+
const simulateContractReturnType: Awaited<ReturnType<typeof publicClient.simulateContract>> = {} as Awaited<
699+
ReturnType<typeof publicClient.simulateContract>
700+
>;
701+
return Promise.resolve(simulateContractReturnType);
702+
});
703+
704+
const walletClient = createWalletClient({
705+
chain: bsc,
706+
transport: http(),
707+
account: privateKeyToAccount(process.env.PRIVATE_KEY as Hex),
708+
});
709+
710+
const walletClientWriteContractSpy = vi.spyOn(walletClient, 'writeContract').mockImplementation(() => {
711+
const writeContractReturnType = {} as Awaited<ReturnType<typeof walletClient.writeContract>>;
712+
return Promise.resolve(writeContractReturnType);
713+
});
714+
715+
const txHash = await client.executeQuote({
716+
quote: mockQuote,
717+
walletClient,
718+
publicClient: publicClient as PublicClient<Transport>,
719+
});
720+
721+
expect(txHash).toBeDefined();
722+
expect(publicClientReadContractSpy).toHaveBeenCalledTimes(0);
723+
expect(publicClientWaitForTransactionReceiptSpy).toHaveBeenCalledTimes(1);
724+
expect(publicClientSimulateContractSpy).toHaveBeenCalledTimes(1);
725+
expect(walletClientWriteContractSpy).toHaveBeenCalledTimes(1);
726+
}, 120000);
569727
});
570728

571729
/**
@@ -633,7 +791,7 @@ class ScureBitcoinSigner implements BitcoinSigner {
633791
class TestLayerZeroGatewayClient extends (await import('../src/gateway/layerzero')).LayerZeroGatewayClient {
634792
_publicClient: any;
635793
constructor(chainId: number, l0Client: any, publicClient: any) {
636-
super(chainId);
794+
super();
637795
// @ts-ignore
638796
this.l0Client = l0Client;
639797
this._publicClient = publicClient;

0 commit comments

Comments
 (0)