├── .gitignore ├── README.md ├── contracts ├── LZEndpointMock.sol └── OmniChat.sol ├── deploy └── omniChat.js ├── hardhat.config.js ├── package-lock.json ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | snaps 7 | 8 | #Hardhat files 9 | cache 10 | artifacts 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LayerZero OmniChat tutorial 2 | 3 | ## Spin up environment 4 | 5 | Create `.env` file and pass your private keys for each chain 6 | 7 | ```bash 8 | RINKEBY_ACCOUNTS='YOUR_PRIVATE_KEY' 9 | FUJI_ACCOUNTS='YOUR_PRIVATE_KEY' 10 | ``` -------------------------------------------------------------------------------- /contracts/LZEndpointMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.4; 4 | pragma abicoder v2; 5 | 6 | import "@layerzerolabs/solidity-examples/contracts/interfaces/ILayerZeroReceiver.sol"; 7 | import "@layerzerolabs/solidity-examples/contracts/interfaces/ILayerZeroEndpoint.sol"; 8 | 9 | /* 10 | mocking multi endpoint connection. 11 | - send() will short circuit to lzReceive() directly 12 | - no reentrancy guard. the real LayerZero endpoint on main net has a send and receive guard, respectively. 13 | if we run a ping-pong-like application, the recursive call might use all gas limit in the block. 14 | - not using any messaging library, hence all messaging library func, e.g. estimateFees, version, will not work 15 | */ 16 | contract LZEndpointMock is ILayerZeroEndpoint { 17 | mapping(address => address) public lzEndpointLookup; 18 | 19 | uint16 public mockChainId; 20 | address payable public mockOracle; 21 | address payable public mockRelayer; 22 | uint public mockBlockConfirmations; 23 | uint16 public mockLibraryVersion; 24 | uint public mockStaticNativeFee; 25 | uint16 public mockLayerZeroVersion; 26 | uint public nativeFee; 27 | uint public zroFee; 28 | bool nextMsgBLocked; 29 | 30 | struct StoredPayload { 31 | uint64 payloadLength; 32 | address dstAddress; 33 | bytes32 payloadHash; 34 | } 35 | 36 | struct QueuedPayload { 37 | address dstAddress; 38 | uint64 nonce; 39 | bytes payload; 40 | } 41 | 42 | // inboundNonce = [srcChainId][srcAddress]. 43 | mapping(uint16 => mapping(bytes => uint64)) public inboundNonce; 44 | // outboundNonce = [dstChainId][srcAddress]. 45 | mapping(uint16 => mapping(address => uint64)) public outboundNonce; 46 | // storedPayload = [srcChainId][srcAddress] 47 | mapping(uint16 => mapping(bytes => StoredPayload)) public storedPayload; 48 | // msgToDeliver = [srcChainId][srcAddress] 49 | mapping(uint16 => mapping(bytes => QueuedPayload[])) public msgsToDeliver; 50 | 51 | event UaForceResumeReceive(uint16 chainId, bytes srcAddress); 52 | event PayloadCleared(uint16 srcChainId, bytes srcAddress, uint64 nonce, address dstAddress); 53 | event PayloadStored(uint16 srcChainId, bytes srcAddress, address dstAddress, uint64 nonce, bytes payload, bytes reason); 54 | 55 | constructor(uint16 _chainId) { 56 | mockStaticNativeFee = 42; 57 | mockLayerZeroVersion = 1; 58 | mockChainId = _chainId; 59 | } 60 | 61 | // mock helper to set the value returned by `estimateNativeFees` 62 | function setEstimatedFees(uint _nativeFee, uint _zroFee) public { 63 | nativeFee = _nativeFee; 64 | zroFee = _zroFee; 65 | } 66 | 67 | function getChainId() external view override returns (uint16) { 68 | return mockChainId; 69 | } 70 | 71 | function setDestLzEndpoint(address destAddr, address lzEndpointAddr) external { 72 | lzEndpointLookup[destAddr] = lzEndpointAddr; 73 | } 74 | 75 | function send( 76 | uint16 _chainId, 77 | bytes calldata _destination, 78 | bytes calldata _payload, 79 | address payable, // _refundAddress 80 | address, // _zroPaymentAddress 81 | bytes memory _adapterParams 82 | ) external payable override { 83 | address destAddr = packedBytesToAddr(_destination); 84 | address lzEndpoint = lzEndpointLookup[destAddr]; 85 | 86 | require(lzEndpoint != address(0), "LayerZeroMock: destination LayerZero Endpoint not found"); 87 | 88 | require(msg.value >= nativeFee * _payload.length, "LayerZeroMock: not enough native for fees"); 89 | 90 | uint64 nonce; 91 | { 92 | nonce = ++outboundNonce[_chainId][msg.sender]; 93 | } 94 | 95 | // Mock the relayer paying the dstNativeAddr the amount of extra native token 96 | { 97 | uint extraGas; 98 | uint dstNative; 99 | address dstNativeAddr; 100 | assembly { 101 | extraGas := mload(add(_adapterParams, 34)) 102 | dstNative := mload(add(_adapterParams, 66)) 103 | dstNativeAddr := mload(add(_adapterParams, 86)) 104 | } 105 | 106 | // to simulate actually sending the ether, add a transfer call and ensure the LZEndpointMock contract has an ether balance 107 | } 108 | 109 | bytes memory bytesSourceUserApplicationAddr = addrToPackedBytes(address(msg.sender)); // cast this address to bytes 110 | 111 | // not using the extra gas parameter because this is a single tx call, not split between different chains 112 | // LZEndpointMock(lzEndpoint).receivePayload(mockChainId, bytesSourceUserApplicationAddr, destAddr, nonce, extraGas, _payload); 113 | LZEndpointMock(lzEndpoint).receivePayload(mockChainId, bytesSourceUserApplicationAddr, destAddr, nonce, 0, _payload); 114 | } 115 | 116 | function receivePayload( 117 | uint16 _srcChainId, 118 | bytes calldata _srcAddress, 119 | address _dstAddress, 120 | uint64 _nonce, 121 | uint, /*_gasLimit*/ 122 | bytes calldata _payload 123 | ) external override { 124 | StoredPayload storage sp = storedPayload[_srcChainId][_srcAddress]; 125 | 126 | // assert and increment the nonce. no message shuffling 127 | require(_nonce == ++inboundNonce[_srcChainId][_srcAddress], "LayerZero: wrong nonce"); 128 | 129 | // queue the following msgs inside of a stack to simulate a successful send on src, but not fully delivered on dst 130 | if (sp.payloadHash != bytes32(0)) { 131 | QueuedPayload[] storage msgs = msgsToDeliver[_srcChainId][_srcAddress]; 132 | QueuedPayload memory newMsg = QueuedPayload(_dstAddress, _nonce, _payload); 133 | 134 | // warning, might run into gas issues trying to forward through a bunch of queued msgs 135 | // shift all the msgs over so we can treat this like a fifo via array.pop() 136 | if (msgs.length > 0) { 137 | // extend the array 138 | msgs.push(newMsg); 139 | 140 | // shift all the indexes up for pop() 141 | for (uint i = 0; i < msgs.length - 1; i++) { 142 | msgs[i + 1] = msgs[i]; 143 | } 144 | 145 | // put the newMsg at the bottom of the stack 146 | msgs[0] = newMsg; 147 | } else { 148 | msgs.push(newMsg); 149 | } 150 | } else if (nextMsgBLocked) { 151 | storedPayload[_srcChainId][_srcAddress] = StoredPayload(uint64(_payload.length), _dstAddress, keccak256(_payload)); 152 | emit PayloadStored(_srcChainId, _srcAddress, _dstAddress, _nonce, _payload, bytes("")); 153 | // ensure the next msgs that go through are no longer blocked 154 | nextMsgBLocked = false; 155 | } else { 156 | // we ignore the gas limit because this call is made in one tx due to being "same chain" 157 | // ILayerZeroReceiver(_dstAddress).lzReceive{gas: _gasLimit}(_srcChainId, _srcAddress, _nonce, _payload); // invoke lzReceive 158 | ILayerZeroReceiver(_dstAddress).lzReceive(_srcChainId, _srcAddress, _nonce, _payload); // invoke lzReceive 159 | } 160 | } 161 | 162 | // used to simulate messages received get stored as a payload 163 | function blockNextMsg() external { 164 | nextMsgBLocked = true; 165 | } 166 | 167 | function getLengthOfQueue(uint16 _srcChainId, bytes calldata _srcAddress) external view returns (uint) { 168 | return msgsToDeliver[_srcChainId][_srcAddress].length; 169 | } 170 | 171 | // @notice gets a quote in source native gas, for the amount that send() requires to pay for message delivery 172 | // @param _dstChainId - the destination chain identifier 173 | // @param _userApplication - the user app address on this EVM chain 174 | // @param _payload - the custom message to send over LayerZero 175 | // @param _payInZRO - if false, user app pays the protocol fee in native token 176 | // @param _adapterParam - parameters for the adapter service, e.g. send some dust native token to dstChain 177 | function estimateFees(uint16, address, bytes memory _payload, bool, bytes memory) external view override returns (uint _nativeFee, uint _zroFee) { 178 | _nativeFee = nativeFee * _payload.length; 179 | _zroFee = zroFee; 180 | } 181 | 182 | // give 20 bytes, return the decoded address 183 | function packedBytesToAddr(bytes calldata _b) public pure returns (address) { 184 | address addr; 185 | assembly { 186 | let ptr := mload(0x40) 187 | calldatacopy(ptr, sub(_b.offset, 2), add(_b.length, 2)) 188 | addr := mload(sub(ptr, 10)) 189 | } 190 | return addr; 191 | } 192 | 193 | // given an address, return the 20 bytes 194 | function addrToPackedBytes(address _a) public pure returns (bytes memory) { 195 | bytes memory data = abi.encodePacked(_a); 196 | return data; 197 | } 198 | 199 | function setConfig( 200 | uint16, /*_version*/ 201 | uint16, /*_chainId*/ 202 | uint, /*_configType*/ 203 | bytes memory /*_config*/ 204 | ) external override {} 205 | 206 | function getConfig( 207 | uint16, /*_version*/ 208 | uint16, /*_chainId*/ 209 | address, /*_ua*/ 210 | uint /*_configType*/ 211 | ) external pure override returns (bytes memory) { 212 | return ""; 213 | } 214 | 215 | function setSendVersion( 216 | uint16 /*version*/ 217 | ) external override {} 218 | 219 | function setReceiveVersion( 220 | uint16 /*version*/ 221 | ) external override {} 222 | 223 | function getSendVersion( 224 | address /*_userApplication*/ 225 | ) external pure override returns (uint16) { 226 | return 1; 227 | } 228 | 229 | function getReceiveVersion( 230 | address /*_userApplication*/ 231 | ) external pure override returns (uint16) { 232 | return 1; 233 | } 234 | 235 | function getInboundNonce(uint16 _chainID, bytes calldata _srcAddress) external view override returns (uint64) { 236 | return inboundNonce[_chainID][_srcAddress]; 237 | } 238 | 239 | function getOutboundNonce(uint16 _chainID, address _srcAddress) external view override returns (uint64) { 240 | return outboundNonce[_chainID][_srcAddress]; 241 | } 242 | 243 | // simulates the relayer pushing through the rest of the msgs that got delayed due to the stored payload 244 | function _clearMsgQue(uint16 _srcChainId, bytes calldata _srcAddress) internal { 245 | QueuedPayload[] storage msgs = msgsToDeliver[_srcChainId][_srcAddress]; 246 | 247 | // warning, might run into gas issues trying to forward through a bunch of queued msgs 248 | while (msgs.length > 0) { 249 | QueuedPayload memory payload = msgs[msgs.length - 1]; 250 | ILayerZeroReceiver(payload.dstAddress).lzReceive(_srcChainId, _srcAddress, payload.nonce, payload.payload); 251 | msgs.pop(); 252 | } 253 | } 254 | 255 | function forceResumeReceive(uint16 _srcChainId, bytes calldata _srcAddress) external override { 256 | StoredPayload storage sp = storedPayload[_srcChainId][_srcAddress]; 257 | // revert if no messages are cached. safeguard malicious UA behaviour 258 | require(sp.payloadHash != bytes32(0), "LayerZero: no stored payload"); 259 | require(sp.dstAddress == msg.sender, "LayerZero: invalid caller"); 260 | 261 | // empty the storedPayload 262 | sp.payloadLength = 0; 263 | sp.dstAddress = address(0); 264 | sp.payloadHash = bytes32(0); 265 | 266 | emit UaForceResumeReceive(_srcChainId, _srcAddress); 267 | 268 | // resume the receiving of msgs after we force clear the "stuck" msg 269 | _clearMsgQue(_srcChainId, _srcAddress); 270 | } 271 | 272 | function retryPayload(uint16 _srcChainId, bytes calldata _srcAddress, bytes calldata _payload) external override { 273 | StoredPayload storage sp = storedPayload[_srcChainId][_srcAddress]; 274 | require(sp.payloadHash != bytes32(0), "LayerZero: no stored payload"); 275 | require(_payload.length == sp.payloadLength && keccak256(_payload) == sp.payloadHash, "LayerZero: invalid payload"); 276 | 277 | address dstAddress = sp.dstAddress; 278 | // empty the storedPayload 279 | sp.payloadLength = 0; 280 | sp.dstAddress = address(0); 281 | sp.payloadHash = bytes32(0); 282 | 283 | uint64 nonce = inboundNonce[_srcChainId][_srcAddress]; 284 | 285 | ILayerZeroReceiver(dstAddress).lzReceive(_srcChainId, _srcAddress, nonce, _payload); 286 | emit PayloadCleared(_srcChainId, _srcAddress, nonce, dstAddress); 287 | } 288 | 289 | function hasStoredPayload(uint16 _srcChainId, bytes calldata _srcAddress) external view override returns (bool) { 290 | StoredPayload storage sp = storedPayload[_srcChainId][_srcAddress]; 291 | return sp.payloadHash != bytes32(0); 292 | } 293 | 294 | function isSendingPayload() external pure override returns (bool) { 295 | return false; 296 | } 297 | 298 | function isReceivingPayload() external pure override returns (bool) { 299 | return false; 300 | } 301 | 302 | function getSendLibraryAddress(address) external view override returns (address) { 303 | return address(this); 304 | } 305 | 306 | function getReceiveLibraryAddress(address) external view override returns (address) { 307 | return address(this); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /contracts/OmniChat.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@layerzerolabs/solidity-examples/contracts/lzApp/NonblockingLzApp.sol"; 5 | import "hardhat/console.sol"; 6 | contract OmniChat is NonblockingLzApp { 7 | address public ten; 8 | struct Message { 9 | address from; 10 | address to; 11 | uint timestamp; 12 | string text; 13 | } 14 | mapping(bytes32 => Message[]) messages; 15 | 16 | constructor(address _lzEndpoint) NonblockingLzApp(_lzEndpoint) { 17 | ten = _lzEndpoint; 18 | } 19 | 20 | function sendMessage(uint16 dstChainId, address dstAccountAddress, string calldata text) external payable { 21 | bytes memory payload = abi.encode(msg.sender, dstAccountAddress, block.timestamp, text); 22 | _lzSend(dstChainId, payload, payable(msg.sender), address(0x0), bytes("")); 23 | } 24 | 25 | function _nonblockingLzReceive(uint16 srcChainId, bytes memory, uint64, bytes memory payload) internal override { 26 | (address from, address to, uint timestamp, string memory text) = abi.decode(payload, (address, address, uint, string)); 27 | // console.log(srcChainId); 28 | // console.logAddress(from); 29 | Message memory message = Message(from, to, timestamp, text); 30 | bytes32 chatId = getChatId(srcChainId, from, to); 31 | 32 | messages[chatId].push(message); 33 | } 34 | 35 | function getMessages(uint16 chainId, address counterpartAddress) external view returns(Message[] memory) { 36 | bytes32 chatId = getChatId(chainId, msg.sender, counterpartAddress); 37 | console.logAddress(msg.sender); 38 | console.logAddress(counterpartAddress); 39 | return messages[chatId]; 40 | } 41 | 42 | function getChatId(uint16 chainId, address address1, address address2) internal pure returns(bytes32) { 43 | (address addressA, address addressB) = address1 < address2 ? (address1, address2) : (address2, address1); 44 | return keccak256(abi.encode(chainId, addressA, addressB)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /deploy/omniChat.js: -------------------------------------------------------------------------------- 1 | const hre = require("hardhat"); 2 | const { LZ_ADDRESS } = require("@layerzerolabs/lz-sdk"); 3 | 4 | async function main() { 5 | hre.run("compile"); 6 | const [deployer] = await ethers.getSigners(); 7 | 8 | console.log("Deploying contracts with the account:", deployer.address); 9 | console.log("Account balance:", (await deployer.getBalance()).toString()); 10 | 11 | const OmniChat = await hre.ethers.getContractFactory("OmniChat"); 12 | const endpointAddr = LZ_ADDRESS[hre.network.name]; 13 | console.log(endpointAddr); 14 | const omniChat = await OmniChat.deploy(endpointAddr); 15 | 16 | await omniChat.deployed(); 17 | console.log("Contract deployed to address:", omniChat.address); 18 | } 19 | 20 | main() 21 | .then(() => process.exit(0)) 22 | .catch((error) => { 23 | console.error(error); 24 | process.exit(1); 25 | }); 26 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | require("@nomiclabs/hardhat-waffle"); 3 | 4 | /** 5 | * @param accountsFromEnv Accounts private keys separated with comma 6 | * @returns Array of account to pass to network object 7 | */ 8 | // const getAccounts = (accountsFromEnv) => accountsFromEnv.split(","); 9 | 10 | module.exports = { 11 | solidity: "0.8.4", 12 | networks: { 13 | mumbai: { 14 | url:"http://192.168.113.50:1234", 15 | chainId:1337, 16 | accounts:["39cfe36c6dfd933d32c85b8557585eb408f1aca852ada021dbd7a1455818d0ed"] 17 | }, 18 | fantom: { 19 | url: "http://127.0.0.1:7545", // Localhost (default: none) 20 | chainId: 1337, 21 | accounts:['f94e376b991bf2ff9728792f956048214396f93dfe1bf4158fd8eebdb1eb602f'] 22 | // Any network (default: none) 23 | }, 24 | } 25 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "layer-zero-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@layerzerolabs/lz-sdk": "^0.0.2", 14 | "@layerzerolabs/solidity-examples": "^0.0.2", 15 | "dotenv": "^16.0.1" 16 | }, 17 | "devDependencies": { 18 | "@nomiclabs/hardhat-waffle": "^2.0.3", 19 | "chai": "^4.3.6", 20 | "ethereum-waffle": "^3.4.4", 21 | "ethers": "^5.6.8", 22 | "hardhat": "^2.9.9" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { ethers } = require("hardhat"); 3 | 4 | describe("OmniChat", () => { 5 | beforeEach(async () => { 6 | this.srcChainId = 1; 7 | this.dstChainId = 2; 8 | 9 | const LZEndpointMock = await hre.ethers.getContractFactory("LZEndpointMock"); 10 | this.srcLzEndpointMock = await LZEndpointMock.deploy(this.srcChainId); 11 | this.dstLzEndpointMock = await LZEndpointMock.deploy(this.dstChainId); 12 | // console.log(this.dstLzEndpointMock.address); 13 | // this.mockEstimatedNativeFee = ethers.utils.parseEther("0.001"); 14 | // this.mockEstimatedZroFee = ethers.utils.parseEther("0.00025"); 15 | // await this.srcLzEndpointMock.setEstimatedFees(this.mockEstimatedNativeFee, this.mockEstimatedZroFee); 16 | // await this.dstLzEndpointMock.setEstimatedFees(this.mockEstimatedNativeFee, this.mockEstimatedZroFee); 17 | 18 | const OmniChat = await hre.ethers.getContractFactory("OmniChat"); 19 | this.omniChatA = await OmniChat.deploy(this.srcLzEndpointMock.address); 20 | this.omniChatB = await OmniChat.deploy(this.dstLzEndpointMock.address); 21 | 22 | await this.srcLzEndpointMock.setDestLzEndpoint(this.omniChatB.address, this.dstLzEndpointMock.address); 23 | await this.dstLzEndpointMock.setDestLzEndpoint(this.omniChatA.address, this.srcLzEndpointMock.address); 24 | 25 | await this.omniChatA.setTrustedRemote(this.dstChainId, this.omniChatB.address); 26 | await this.omniChatB.setTrustedRemote(this.srcChainId, this.omniChatA.address); 27 | }); 28 | 29 | it("successfully sends messages", async () => { 30 | const [srcAcc, dstAcc,sd] = await ethers.getSigners(); 31 | // console.log(sd.address); 32 | // await this.omniChatA.sendMessage(this.dstChainId, dstAcc.address, "test message 1", { value: ethers.utils.parseEther("0.5") }); 33 | await this.omniChatB.sendMessage(this.srcChainId, srcAcc.address, "test message 1", { value: ethers.utils.parseEther("0.5") }); 34 | const aMessages = await this.omniChatA.getMessages(this.dstChainId, srcAcc.address); 35 | const bMessages = await this.omniChatB.connect(dstAcc).getMessages(this.srcChainId, srcAcc.address); 36 | console.log(aMessages); 37 | expect(aMessages).to.have.lengthOf(1); 38 | expect(bMessages).to.have.lengthOf(0); 39 | }); 40 | }); --------------------------------------------------------------------------------