Files
hash-wallet/cw_evm/lib/contract/oft.dart
T
586a040e4a CW-1273: Implement USDT0 Bridge (#2855)
* feat: Implement USDT0 Bridging using OFT Standard by Layer Zero. POC available in apps menu.

* feat: Implement USDT0 Bridging using OFT Standard by Layer Zero. POC available in apps menu.

* feat: Implement USDT0 Bridging using OFT Standard by Layer Zero. POC available in apps menu.

* Add transaction history and status polling for USDT0 bridging

* Add  Transaction history and status polling for USDT0 Bridging

* USDT0 Bridging Implementation

* Update Arbitrum USDT token symbol

* fix: Merge conflicts

* fix: Merge conflicts

* chore: Rever formatting

* feat: Implement new flow

* feat: Implement new ui flow

* Add currency to configure

* feat: new ui flow

* Update USDT0 implementation
- Switch to sqlite for storage instead of hive
- Switch bridge history and details to modals
- Switch bridge transfer details page to new tx details ui

* refactor: rearrange modal action buttons

* enhance usdt0 bridge flows

* Update lib/entities/wallet_manager.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
2026-04-15 05:06:05 +02:00

175 lines
5.7 KiB
Dart

import 'dart:typed_data';
import 'package:cw_evm/usdt0/usdt0_quote.dart';
import 'package:hex/hex.dart' as hex;
import 'package:web3dart/web3dart.dart' as web3;
/// Minimal OFT ABI for USDT0 (per docs.usdt0.to).
/// quoteSend(SendParam, bool payInLzToken) returns MessagingFee.
/// send(SendParam, MessagingFee, address refundAddress) payable.
/// SendParam: dstEid, to (bytes32), amountLD (amount to send, token units),
/// minAmountLD (min receive on destination), extraOptions, composeMsg, oftCmd.
const String _oftAbiJson = '''
[
{
"inputs": [
{
"components": [
{"internalType": "uint32", "name": "dstEid", "type": "uint32"},
{"internalType": "bytes32", "name": "to", "type": "bytes32"},
{"internalType": "uint256", "name": "amountLD", "type": "uint256"},
{"internalType": "uint256", "name": "minAmountLD", "type": "uint256"},
{"internalType": "bytes", "name": "extraOptions", "type": "bytes"},
{"internalType": "bytes", "name": "composeMsg", "type": "bytes"},
{"internalType": "bytes", "name": "oftCmd", "type": "bytes"}
],
"internalType": "struct SendParam",
"name": "sendParam",
"type": "tuple"
},
{"internalType": "bool", "name": "payInLzToken", "type": "bool"}
],
"name": "quoteSend",
"outputs": [
{
"components": [
{"internalType": "uint256", "name": "nativeFee", "type": "uint256"},
{"internalType": "uint256", "name": "lzTokenFee", "type": "uint256"}
],
"internalType": "struct MessagingFee",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{"internalType": "uint32", "name": "dstEid", "type": "uint32"},
{"internalType": "bytes32", "name": "to", "type": "bytes32"},
{"internalType": "uint256", "name": "amountLD", "type": "uint256"},
{"internalType": "uint256", "name": "minAmountLD", "type": "uint256"},
{"internalType": "bytes", "name": "extraOptions", "type": "bytes"},
{"internalType": "bytes", "name": "composeMsg", "type": "bytes"},
{"internalType": "bytes", "name": "oftCmd", "type": "bytes"}
],
"internalType": "struct SendParam",
"name": "_sendParam",
"type": "tuple"
},
{
"components": [
{"internalType": "uint256", "name": "nativeFee", "type": "uint256"},
{"internalType": "uint256", "name": "lzTokenFee", "type": "uint256"}
],
"internalType": "struct MessagingFee",
"name": "_fee",
"type": "tuple"
},
{"internalType": "address", "name": "_refundAddress", "type": "address"}
],
"name": "send",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
]
''';
final web3.ContractAbi oftContractAbi = web3.ContractAbi.fromJson(_oftAbiJson, 'OFT');
/// OFT contract wrapper for quoteSend (view) and send (payable).
class OFT {
OFT({
required web3.EthereumAddress address,
required web3.Web3Client client,
}) : _address = address,
_client = client;
final web3.EthereumAddress _address;
final web3.Web3Client _client;
/// Calls quoteSend and returns MessagingFee (nativeFee, lzTokenFee).
Future<USDT0Quote> quoteSend({
required int dstEid,
required List<int> toBytes32,
required BigInt amountLD,
required BigInt minAmountLD,
Uint8List? extraOptions,
Uint8List? composeMsg,
Uint8List? oftCmd,
bool payInLzToken = false,
}) async {
final contract = web3.DeployedContract(
oftContractAbi,
_address,
);
final fn = contract.function('quoteSend');
final sendParam = [
BigInt.from(dstEid),
Uint8List.fromList(toBytes32),
amountLD,
minAmountLD,
extraOptions ?? Uint8List(0),
composeMsg ?? Uint8List(0),
oftCmd ?? Uint8List(0),
];
final params = [sendParam, payInLzToken];
final result = await _client.call(
contract: contract,
function: fn,
params: params,
);
final msgFee = result.first as List<dynamic>;
final nativeFee = msgFee[0] as BigInt;
final lzTokenFee = msgFee[1] as BigInt;
return USDT0Quote(nativeFee: nativeFee, lzTokenFee: lzTokenFee);
}
/// Encodes send(SendParam, MessagingFee, refundAddress) for transaction.
/// Returns (dataHex, valueWei) where valueWei is the native fee to send.
({String dataHex, BigInt valueWei}) encodeSend({
required int dstEid,
required List<int> toBytes32,
required BigInt amountLD,
required BigInt minAmountLD,
required BigInt nativeFee,
required BigInt lzTokenFee,
required String refundAddress,
Uint8List? extraOptions,
Uint8List? composeMsg,
Uint8List? oftCmd,
}) {
final contract = web3.DeployedContract(oftContractAbi, _address);
final fn = contract.function('send');
final sendParam = [
BigInt.from(dstEid),
Uint8List.fromList(toBytes32),
amountLD,
minAmountLD,
extraOptions ?? Uint8List(0),
composeMsg ?? Uint8List(0),
oftCmd ?? Uint8List(0),
];
final fee = [nativeFee, lzTokenFee];
final refund = web3.EthereumAddress.fromHex(refundAddress);
final encoded = fn.encodeCall([sendParam, fee, refund]);
final dataHex = '0x${hex.HEX.encode(encoded)}';
return (dataHex: dataHex, valueWei: nativeFee);
}
}
/// Converts EVM address (0x + 40 hex) to bytes32 (left-padded).
List<int> addressToBytes32(String address) {
final clean = address.startsWith('0x') ? address.substring(2) : address;
if (clean.length != 40) throw ArgumentError('Invalid address length');
return hex.HEX.decode('000000000000000000000000$clean');
}