| 1 | import 'dart:typed_data'; |
| 2 | |
| 3 | import 'package:cw_evm/usdt0/usdt0_quote.dart'; |
| 4 | import 'package:hex/hex.dart' as hex; |
| 5 | import 'package:web3dart/web3dart.dart' as web3; |
| 6 | |
| 7 | /// Minimal OFT ABI for USDT0 (per docs.usdt0.to). |
| 8 | /// quoteSend(SendParam, bool payInLzToken) returns MessagingFee. |
| 9 | /// send(SendParam, MessagingFee, address refundAddress) payable. |
| 10 | /// SendParam: dstEid, to (bytes32), amountLD (amount to send, token units), |
| 11 | /// minAmountLD (min receive on destination), extraOptions, composeMsg, oftCmd. |
| 12 | const String _oftAbiJson = ''' |
| 13 | [ |
| 14 | { |
| 15 | "inputs": [ |
| 16 | { |
| 17 | "components": [ |
| 18 | {"internalType": "uint32", "name": "dstEid", "type": "uint32"}, |
| 19 | {"internalType": "bytes32", "name": "to", "type": "bytes32"}, |
| 20 | {"internalType": "uint256", "name": "amountLD", "type": "uint256"}, |
| 21 | {"internalType": "uint256", "name": "minAmountLD", "type": "uint256"}, |
| 22 | {"internalType": "bytes", "name": "extraOptions", "type": "bytes"}, |
| 23 | {"internalType": "bytes", "name": "composeMsg", "type": "bytes"}, |
| 24 | {"internalType": "bytes", "name": "oftCmd", "type": "bytes"} |
| 25 | ], |
| 26 | "internalType": "struct SendParam", |
| 27 | "name": "sendParam", |
| 28 | "type": "tuple" |
| 29 | }, |
| 30 | {"internalType": "bool", "name": "payInLzToken", "type": "bool"} |
| 31 | ], |
| 32 | "name": "quoteSend", |
| 33 | "outputs": [ |
| 34 | { |
| 35 | "components": [ |
| 36 | {"internalType": "uint256", "name": "nativeFee", "type": "uint256"}, |
| 37 | {"internalType": "uint256", "name": "lzTokenFee", "type": "uint256"} |
| 38 | ], |
| 39 | "internalType": "struct MessagingFee", |
| 40 | "name": "", |
| 41 | "type": "tuple" |
| 42 | } |
| 43 | ], |
| 44 | "stateMutability": "view", |
| 45 | "type": "function" |
| 46 | }, |
| 47 | { |
| 48 | "inputs": [ |
| 49 | { |
| 50 | "components": [ |
| 51 | {"internalType": "uint32", "name": "dstEid", "type": "uint32"}, |
| 52 | {"internalType": "bytes32", "name": "to", "type": "bytes32"}, |
| 53 | {"internalType": "uint256", "name": "amountLD", "type": "uint256"}, |
| 54 | {"internalType": "uint256", "name": "minAmountLD", "type": "uint256"}, |
| 55 | {"internalType": "bytes", "name": "extraOptions", "type": "bytes"}, |
| 56 | {"internalType": "bytes", "name": "composeMsg", "type": "bytes"}, |
| 57 | {"internalType": "bytes", "name": "oftCmd", "type": "bytes"} |
| 58 | ], |
| 59 | "internalType": "struct SendParam", |
| 60 | "name": "_sendParam", |
| 61 | "type": "tuple" |
| 62 | }, |
| 63 | { |
| 64 | "components": [ |
| 65 | {"internalType": "uint256", "name": "nativeFee", "type": "uint256"}, |
| 66 | {"internalType": "uint256", "name": "lzTokenFee", "type": "uint256"} |
| 67 | ], |
| 68 | "internalType": "struct MessagingFee", |
| 69 | "name": "_fee", |
| 70 | "type": "tuple" |
| 71 | }, |
| 72 | {"internalType": "address", "name": "_refundAddress", "type": "address"} |
| 73 | ], |
| 74 | "name": "send", |
| 75 | "outputs": [], |
| 76 | "stateMutability": "payable", |
| 77 | "type": "function" |
| 78 | } |
| 79 | ] |
| 80 | '''; |
| 81 | |
| 82 | final web3.ContractAbi oftContractAbi = web3.ContractAbi.fromJson(_oftAbiJson, 'OFT'); |
| 83 | |
| 84 | /// OFT contract wrapper for quoteSend (view) and send (payable). |
| 85 | class OFT { |
| 86 | OFT({ |
| 87 | required web3.EthereumAddress address, |
| 88 | required web3.Web3Client client, |
| 89 | }) : _address = address, |
| 90 | _client = client; |
| 91 | |
| 92 | final web3.EthereumAddress _address; |
| 93 | final web3.Web3Client _client; |
| 94 | |
| 95 | /// Calls quoteSend and returns MessagingFee (nativeFee, lzTokenFee). |
| 96 | Future<USDT0Quote> quoteSend({ |
| 97 | required int dstEid, |
| 98 | required List<int> toBytes32, |
| 99 | required BigInt amountLD, |
| 100 | required BigInt minAmountLD, |
| 101 | Uint8List? extraOptions, |
| 102 | Uint8List? composeMsg, |
| 103 | Uint8List? oftCmd, |
| 104 | bool payInLzToken = false, |
| 105 | }) async { |
| 106 | final contract = web3.DeployedContract( |
| 107 | oftContractAbi, |
| 108 | _address, |
| 109 | ); |
| 110 | final fn = contract.function('quoteSend'); |
| 111 | final sendParam = [ |
| 112 | BigInt.from(dstEid), |
| 113 | Uint8List.fromList(toBytes32), |
| 114 | amountLD, |
| 115 | minAmountLD, |
| 116 | extraOptions ?? Uint8List(0), |
| 117 | composeMsg ?? Uint8List(0), |
| 118 | oftCmd ?? Uint8List(0), |
| 119 | ]; |
| 120 | final params = [sendParam, payInLzToken]; |
| 121 | |
| 122 | final result = await _client.call( |
| 123 | contract: contract, |
| 124 | function: fn, |
| 125 | params: params, |
| 126 | ); |
| 127 | |
| 128 | final msgFee = result.first as List<dynamic>; |
| 129 | final nativeFee = msgFee[0] as BigInt; |
| 130 | final lzTokenFee = msgFee[1] as BigInt; |
| 131 | return USDT0Quote(nativeFee: nativeFee, lzTokenFee: lzTokenFee); |
| 132 | } |
| 133 | |
| 134 | /// Encodes send(SendParam, MessagingFee, refundAddress) for transaction. |
| 135 | /// Returns (dataHex, valueWei) where valueWei is the native fee to send. |
| 136 | ({String dataHex, BigInt valueWei}) encodeSend({ |
| 137 | required int dstEid, |
| 138 | required List<int> toBytes32, |
| 139 | required BigInt amountLD, |
| 140 | required BigInt minAmountLD, |
| 141 | required BigInt nativeFee, |
| 142 | required BigInt lzTokenFee, |
| 143 | required String refundAddress, |
| 144 | Uint8List? extraOptions, |
| 145 | Uint8List? composeMsg, |
| 146 | Uint8List? oftCmd, |
| 147 | }) { |
| 148 | final contract = web3.DeployedContract(oftContractAbi, _address); |
| 149 | final fn = contract.function('send'); |
| 150 | final sendParam = [ |
| 151 | BigInt.from(dstEid), |
| 152 | Uint8List.fromList(toBytes32), |
| 153 | amountLD, |
| 154 | minAmountLD, |
| 155 | extraOptions ?? Uint8List(0), |
| 156 | composeMsg ?? Uint8List(0), |
| 157 | oftCmd ?? Uint8List(0), |
| 158 | ]; |
| 159 | final fee = [nativeFee, lzTokenFee]; |
| 160 | final refund = web3.EthereumAddress.fromHex(refundAddress); |
| 161 | final encoded = fn.encodeCall([sendParam, fee, refund]); |
| 162 | final dataHex = '0x${hex.HEX.encode(encoded)}'; |
| 163 | return (dataHex: dataHex, valueWei: nativeFee); |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | /// Converts EVM address (0x + 40 hex) to bytes32 (left-padded). |
| 168 | List<int> addressToBytes32(String address) { |
| 169 | final clean = address.startsWith('0x') ? address.substring(2) : address; |
| 170 | |
| 171 | if (clean.length != 40) throw ArgumentError('Invalid address length'); |
| 172 | |
| 173 | return hex.HEX.decode('000000000000000000000000$clean'); |
| 174 | } |