| 1 | import 'package:cw_core/amount/money.dart'; |
| 2 | import 'package:cw_core/erc20_token.dart'; |
| 3 | import 'package:cw_core/pending_transaction.dart'; |
| 4 | import 'package:cw_evm/contract/erc20.dart'; |
| 5 | import 'package:cw_evm/contract/oft.dart'; |
| 6 | import 'package:cw_evm/evm_chain_transaction_priority.dart'; |
| 7 | import 'package:cw_evm/utils/evm_chain_utils.dart'; |
| 8 | import 'package:cw_evm/evm_chain_wallet.dart'; |
| 9 | import 'package:cw_evm/usdt0/usdt0_config.dart'; |
| 10 | import 'package:cw_evm/usdt0/usdt0_quote.dart'; |
| 11 | import 'package:web3dart/web3dart.dart' as web3; |
| 12 | |
| 13 | class USDT0Service { |
| 14 | static Future<USDT0Quote> quoteCrossChainTransfer({ |
| 15 | required web3.Web3Client client, |
| 16 | required int sourceChainId, |
| 17 | required int destinationChainId, |
| 18 | required BigInt amount, |
| 19 | required String recipientAddress, |
| 20 | BigInt? minAmount, |
| 21 | }) async { |
| 22 | final min = minAmount ?? BigInt.zero; |
| 23 | final oftAddress = USDT0Config.getOftContractAddress(sourceChainId); |
| 24 | final dstEid = USDT0Config.getEndpointId(destinationChainId); |
| 25 | |
| 26 | if (oftAddress == null || dstEid == null) { |
| 27 | throw Exception( |
| 28 | 'USDT0 not supported for chain $sourceChainId -> $destinationChainId', |
| 29 | ); |
| 30 | } |
| 31 | |
| 32 | final toBytes32 = addressToBytes32(recipientAddress); |
| 33 | final oft = OFT( |
| 34 | address: web3.EthereumAddress.fromHex(oftAddress), |
| 35 | client: client, |
| 36 | ); |
| 37 | |
| 38 | return oft.quoteSend( |
| 39 | dstEid: dstEid, |
| 40 | toBytes32: toBytes32, |
| 41 | amountLD: amount, |
| 42 | minAmountLD: min, |
| 43 | payInLzToken: false, |
| 44 | ); |
| 45 | } |
| 46 | |
| 47 | static Future<PendingTransaction> executeCrossChainTransfer({ |
| 48 | required EVMChainWallet wallet, |
| 49 | required int sourceChainId, |
| 50 | required int destinationChainId, |
| 51 | required BigInt amount, |
| 52 | required String recipientAddress, |
| 53 | required USDT0Quote quote, |
| 54 | required Erc20Token token, |
| 55 | required EVMChainTransactionPriority priority, |
| 56 | bool useBlinkProtection = true, |
| 57 | }) async { |
| 58 | final oftAddress = USDT0Config.getOftContractAddress(sourceChainId); |
| 59 | final dstEid = USDT0Config.getEndpointId(destinationChainId); |
| 60 | |
| 61 | if (oftAddress == null || dstEid == null) { |
| 62 | throw Exception( |
| 63 | 'USDT0 not supported for chain $sourceChainId -> $destinationChainId', |
| 64 | ); |
| 65 | } |
| 66 | |
| 67 | if (sourceChainId == USDT0Config.ethereumChainId) { |
| 68 | final adapter = USDT0Config.getOftAdapterAddress(sourceChainId); |
| 69 | if (adapter != null) { |
| 70 | final needsApproval = await _isApprovalRequired( |
| 71 | wallet: wallet, |
| 72 | token: token, |
| 73 | spender: adapter, |
| 74 | amount: amount, |
| 75 | ); |
| 76 | |
| 77 | if (needsApproval) { |
| 78 | final maxUint = BigInt.parse( |
| 79 | 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', |
| 80 | radix: 16, |
| 81 | ); |
| 82 | |
| 83 | final pendingApproval = await wallet.createApprovalTransaction( |
| 84 | Money(maxUint, token), |
| 85 | adapter, |
| 86 | priority, |
| 87 | useBlinkProtection: useBlinkProtection, |
| 88 | ); |
| 89 | await pendingApproval.commit(); |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | final client = wallet.getWeb3Client(); |
| 95 | if (client == null) { |
| 96 | throw StateError('Wallet not connected to node'); |
| 97 | } |
| 98 | |
| 99 | final toBytes32 = addressToBytes32(recipientAddress); |
| 100 | final oft = OFT( |
| 101 | address: web3.EthereumAddress.fromHex(oftAddress), |
| 102 | client: client, |
| 103 | ); |
| 104 | |
| 105 | final encoded = oft.encodeSend( |
| 106 | dstEid: dstEid, |
| 107 | toBytes32: toBytes32, |
| 108 | amountLD: amount, |
| 109 | minAmountLD: BigInt.zero, |
| 110 | nativeFee: quote.nativeFee, |
| 111 | lzTokenFee: quote.lzTokenFee, |
| 112 | refundAddress: wallet.walletAddresses.primaryAddress, |
| 113 | ); |
| 114 | |
| 115 | return wallet.createCallDataTransaction( |
| 116 | oftAddress, |
| 117 | encoded.dataHex, |
| 118 | Money(encoded.valueWei, token), |
| 119 | priority, |
| 120 | token.contractAddress, |
| 121 | amount, |
| 122 | useBlinkProtection: useBlinkProtection, |
| 123 | ); |
| 124 | } |
| 125 | |
| 126 | static Future<bool> _isApprovalRequired({ |
| 127 | required EVMChainWallet wallet, |
| 128 | required Erc20Token token, |
| 129 | required String spender, |
| 130 | required BigInt amount, |
| 131 | }) async { |
| 132 | final client = wallet.getWeb3Client(); |
| 133 | if (client == null) return true; |
| 134 | |
| 135 | final erc20 = ERC20( |
| 136 | address: web3.EthereumAddress.fromHex(token.contractAddress), |
| 137 | client: client, |
| 138 | ); |
| 139 | final current = await erc20.allowance( |
| 140 | web3.EthereumAddress.fromHex(wallet.walletAddresses.primaryAddress), |
| 141 | web3.EthereumAddress.fromHex(spender), |
| 142 | ); |
| 143 | return current < amount; |
| 144 | } |
| 145 | } |