-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: code generation for cross-chain messaging #8
Merged
Merged
Changes from 8 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
76ba583
feat: code gen for messaging
fadeev 9a89717
feat: ccm tasks
fadeev 23de43d
refactor: template processing
fadeev eb6f785
merge main
fadeev 64491c5
remove .DS_Store
fadeev 47cb013
lint
fadeev 84aecd9
fix: tests
fadeev 895d8a6
fix: tests
fadeev cb75fbb
Merge branch 'main' into feat/messaging
fadeev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ coverage.json | |
typechain | ||
typechain-types | ||
dist | ||
.DS_Store | ||
|
||
# Hardhat files | ||
cache | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import * as fs from "fs"; | ||
import * as handlebars from "handlebars"; | ||
import * as path from "path"; | ||
|
||
const prepareData = (args: any) => { | ||
const argsList = args.arguments || []; | ||
const names = argsList.map((i: string) => i.split(":")[0]); | ||
const types = argsList.map((i: string) => { | ||
let parts = i.split(":"); | ||
// If there's a type and it's not empty, use it; if not, default to "bytes32" | ||
let t = | ||
parts.length > 1 && parts[1].trim() !== "" ? parts[1].trim() : "bytes32"; | ||
return t; | ||
}); | ||
const pairs = names.map((v: string, i: string) => [v, types[i]]); | ||
const contractName = sanitizeSolidityFunctionName(args.name); | ||
|
||
return { | ||
args, | ||
arguments: { names, pairs, types }, | ||
contractName, | ||
contractNameUnderscore: camelToUnderscoreUpper(contractName), | ||
}; | ||
}; | ||
|
||
const processTemplatesRecursive = async ( | ||
template: string, | ||
outputDir: string, | ||
data: Record<string, unknown> | ||
): Promise<void> => { | ||
try { | ||
const templateDir = path.resolve( | ||
__dirname, | ||
path.resolve(__dirname, "..", "templates", template) | ||
); | ||
|
||
const files = fs.readdirSync(templateDir); | ||
|
||
for (const file of files) { | ||
const templatePath = path.join(templateDir, file); | ||
|
||
// Compiling filename as a template | ||
const filenameTemplate = handlebars.compile(file); | ||
const filename = filenameTemplate(data); | ||
|
||
// Replacing .hbs extension if the file was a handlebars template | ||
const outputPath = path.join(outputDir, filename.replace(".hbs", "")); | ||
|
||
fs.mkdirSync(path.dirname(outputPath), { recursive: true }); | ||
|
||
if (fs.lstatSync(templatePath).isDirectory()) { | ||
// If file is a directory, recursively process it | ||
await processTemplatesRecursive(templatePath, outputPath, data); | ||
} else if (path.extname(file) === ".hbs") { | ||
const templateContent = fs.readFileSync(templatePath, "utf-8"); | ||
const template = handlebars.compile(templateContent); | ||
const outputContent = template(data); | ||
fs.writeFileSync(outputPath, outputContent); | ||
} else { | ||
fs.copyFileSync(templatePath, outputPath); | ||
} | ||
} | ||
} catch (error) { | ||
console.error(`Error processing templates: ${error}`); | ||
} | ||
}; | ||
|
||
export const processTemplates = async (templateName: string, args: any) => { | ||
processTemplatesRecursive( | ||
templateName, | ||
path.resolve(process.cwd()), | ||
prepareData(args) | ||
); | ||
}; | ||
|
||
const camelToUnderscoreUpper = (input: string): string => { | ||
return input.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase(); | ||
}; | ||
|
||
const sanitizeSolidityFunctionName = (str: string): string => { | ||
// Remove any character that's not alphanumeric or underscore | ||
const cleaned = str.replace(/[^a-zA-Z0-9_]/g, ""); | ||
|
||
// If the first character is a digit, prepend with an underscore | ||
return cleaned.match(/^\d/) ? `_${cleaned}` : cleaned; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import * as fs from "fs"; | ||
import { task } from "hardhat/config"; | ||
import { HardhatRuntimeEnvironment } from "hardhat/types"; | ||
import * as path from "path"; | ||
|
||
import { processTemplates } from "../lib"; | ||
|
||
const main = async (args: any, hre: HardhatRuntimeEnvironment) => { | ||
processTemplates("messaging", args); | ||
|
||
const configPath = path.resolve(process.cwd(), "hardhat.config.ts"); | ||
let hardhatConfigContents = fs.readFileSync(configPath, "utf8"); | ||
|
||
// Add the omnichain tasks to the hardhat.config.ts file | ||
["deploy", "interact"].forEach((task) => { | ||
const content = `import "./tasks/${task}";\n`; | ||
if (!hardhatConfigContents.includes(content)) { | ||
hardhatConfigContents = content + hardhatConfigContents; | ||
} | ||
}); | ||
|
||
fs.writeFileSync(configPath, hardhatConfigContents); | ||
}; | ||
|
||
export const messageTask = task( | ||
"message", | ||
"Generate code for a cross-chain messaging contract", | ||
main | ||
) | ||
.addPositionalParam("name", "Name of the contract") | ||
.addOptionalVariadicPositionalParam( | ||
"arguments", | ||
"Arguments for a crosschain call (e.g. dest:address to:bytes32 output:uint256)" | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.7; | ||
|
||
import "@openzeppelin/contracts/interfaces/IERC20.sol"; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import "@zetachain/protocol-contracts/contracts/evm/tools/ZetaInteractor.sol"; | ||
import "@zetachain/protocol-contracts/contracts/evm/interfaces/ZetaInterfaces.sol"; | ||
|
||
interface {{contractName}}Errors { | ||
error InvalidMessageType(); | ||
} | ||
|
||
contract {{contractName}} is | ||
ZetaInteractor, | ||
ZetaReceiver, | ||
{{contractName}}Errors | ||
{ | ||
bytes32 public constant {{contractNameUnderscore}}_MESSAGE_TYPE = | ||
keccak256("CROSS_CHAIN_{{contractNameUnderscore}}"); | ||
|
||
event {{contractName}}Event({{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}}{{/each}}); | ||
event {{contractName}}RevertedEvent({{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}}{{/each}}); | ||
|
||
ZetaTokenConsumer private immutable _zetaConsumer; | ||
IERC20 internal immutable _zetaToken; | ||
|
||
constructor( | ||
address connectorAddress, | ||
address zetaTokenAddress, | ||
address zetaConsumerAddress | ||
) ZetaInteractor(connectorAddress) { | ||
_zetaToken = IERC20(zetaTokenAddress); | ||
_zetaConsumer = ZetaTokenConsumer(zetaConsumerAddress); | ||
} | ||
|
||
function sendMessage(uint256 destinationChainId{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}} {{this.[0]}}{{/each}}{{/if}}) external payable { | ||
if (!_isValidChainId(destinationChainId)) | ||
revert InvalidDestinationChainId(); | ||
|
||
uint256 crossChainGas = 18 * (10 ** 18); | ||
uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{ | ||
value: msg.value | ||
}(address(this), crossChainGas); | ||
_zetaToken.approve(address(connector), zetaValueAndGas); | ||
|
||
connector.send( | ||
ZetaInterfaces.SendInput({ | ||
destinationChainId: destinationChainId, | ||
destinationAddress: interactorsByChainId[destinationChainId], | ||
destinationGasLimit: 300000, | ||
message: abi.encode({{contractNameUnderscore}}_MESSAGE_TYPE{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[0]}}{{/each}}{{/if}}), | ||
zetaValueAndGas: zetaValueAndGas, | ||
zetaParams: abi.encode("") | ||
}) | ||
); | ||
} | ||
|
||
function onZetaMessage( | ||
ZetaInterfaces.ZetaMessage calldata zetaMessage | ||
) external override isValidMessageCall(zetaMessage) { | ||
/** | ||
* @dev Decode should follow the signature of the message provided to zeta.send. | ||
*/ | ||
(bytes32 messageType{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}} {{this.[0]}}{{/each}}{{/if}}) = abi.decode( | ||
zetaMessage.message, (bytes32{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}}{{/each}}{{/if}}) | ||
); | ||
|
||
/** | ||
* @dev Setting a message type is a useful pattern to distinguish between different messages. | ||
*/ | ||
if (messageType != {{contractNameUnderscore}}_MESSAGE_TYPE) | ||
revert InvalidMessageType(); | ||
|
||
emit {{contractName}}Event({{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[0]}}{{/each}}); | ||
} | ||
|
||
/** | ||
* @dev Called by the Zeta Connector contract when the message fails to be sent. | ||
* Useful to cleanup and leave the application on its initial state. | ||
* Note that the require statements and the functionality are similar to onZetaMessage. | ||
*/ | ||
function onZetaRevert( | ||
ZetaInterfaces.ZetaRevert calldata zetaRevert | ||
) external override isValidRevertCall(zetaRevert) { | ||
(bytes32 messageType{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}} {{this.[0]}}{{/each}}{{/if}}) = abi.decode( | ||
zetaRevert.message, | ||
(bytes32{{#if arguments.pairs}}, {{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[1]}}{{/each}}{{/if}}) | ||
); | ||
|
||
if (messageType != {{contractNameUnderscore}}_MESSAGE_TYPE) | ||
revert InvalidMessageType(); | ||
|
||
emit {{contractName}}RevertedEvent({{#each arguments.pairs}}{{#if @index}}, {{/if}}{{this.[0]}}{{/each}}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { getAddress, getChainId } from "@zetachain/addresses"; | ||
import { ethers } from "ethers"; | ||
import { task } from "hardhat/config"; | ||
import { HardhatRuntimeEnvironment } from "hardhat/types"; | ||
|
||
const contractName = "{{contractName}}"; | ||
|
||
const main = async (args: any, hre: HardhatRuntimeEnvironment) => { | ||
const networks = args.networks.split(","); | ||
// A mapping between network names and deployed contract addresses. | ||
const contracts: { [key: string]: string } = {}; | ||
await Promise.all( | ||
networks.map(async (networkName: string) => { | ||
contracts[networkName] = await deployContract(hre, networkName); | ||
}) | ||
); | ||
|
||
for (const source in contracts) { | ||
await setInteractors(hre, source, contracts); | ||
} | ||
}; | ||
|
||
// Initialize a wallet using a network configuration and a private key from | ||
// environment variables. | ||
const initWallet = (hre: HardhatRuntimeEnvironment, networkName: string) => { | ||
const { url } = hre.config.networks[networkName]; | ||
const provider = new ethers.providers.JsonRpcProvider(url); | ||
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY as string, provider); | ||
|
||
return wallet; | ||
}; | ||
|
||
// Deploy the contract on the specified network. deployContract reads the | ||
// contract artifact, creates a contract factory, and deploys the contract using | ||
// that factory. | ||
const deployContract = async ( | ||
hre: HardhatRuntimeEnvironment, | ||
networkName: string | ||
) => { | ||
const wallet = initWallet(hre, networkName); | ||
const zetaNetwork = "athens"; | ||
const connectorAddress = getAddress({ | ||
address: "connector", | ||
networkName, | ||
zetaNetwork, | ||
}); | ||
const zetaTokenAddress = getAddress({ | ||
address: "zetaToken", | ||
networkName, | ||
zetaNetwork, | ||
}); | ||
const zetaTokenConsumerV2 = getAddress({ | ||
address: "zetaTokenConsumerUniV2", | ||
networkName, | ||
zetaNetwork, | ||
}); | ||
const zetaTokenConsumerV3 = getAddress({ | ||
address: "zetaTokenConsumerUniV3", | ||
andresaiello marked this conversation as resolved.
Show resolved
Hide resolved
|
||
networkName, | ||
zetaNetwork, | ||
}); | ||
|
||
const { abi, bytecode } = await hre.artifacts.readArtifact(contractName); | ||
const factory = new ethers.ContractFactory(abi, bytecode, wallet); | ||
const contract = await factory.deploy( | ||
connectorAddress, | ||
zetaTokenAddress, | ||
zetaTokenConsumerV2 || zetaTokenConsumerV3 | ||
); | ||
|
||
await contract.deployed(); | ||
console.log(` | ||
🚀 Successfully deployed contract on ${networkName}. | ||
📜 Contract address: ${contract.address} | ||
`); | ||
return contract.address; | ||
}; | ||
|
||
// Set interactors for a contract. setInteractors attaches to the contract | ||
// deployed at the specified address, and for every other network, sets the | ||
// deployed contract's address as an interactor. | ||
const setInteractors = async ( | ||
hre: HardhatRuntimeEnvironment, | ||
source: string, | ||
contracts: { [key: string]: string } | ||
) => { | ||
console.log(` | ||
🔗 Setting interactors for a contract on ${source}`); | ||
const wallet = initWallet(hre, source); | ||
|
||
const { abi, bytecode } = await hre.artifacts.readArtifact(contractName); | ||
const factory = new ethers.ContractFactory(abi, bytecode, wallet); | ||
const contract = factory.attach(contracts[source]); | ||
|
||
for (const counterparty in contracts) { | ||
// Skip the destination network if it's the same as the source network. | ||
// For example, we don't need to set an interactor for a contract on | ||
// Goerli if the destination network is also Goerli. | ||
if (counterparty === source) continue; | ||
|
||
const counterpartyContract = hre.ethers.utils.solidityPack( | ||
["address"], | ||
[contracts[counterparty]] | ||
); | ||
const chainId = getChainId(counterparty as any); | ||
await ( | ||
await contract.setInteractorByChainId(chainId, counterpartyContract) | ||
).wait(); | ||
console.log( | ||
`✅ Interactor address for ${chainId} (${counterparty}) is set to ${counterpartyContract}` | ||
); | ||
} | ||
}; | ||
|
||
task("deploy", "Deploy the contract", main).addParam( | ||
"networks", | ||
"Comma separated list of networks to deploy to" | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { task } from "hardhat/config"; | ||
import { HardhatRuntimeEnvironment } from "hardhat/types"; | ||
import { parseEther } from "@ethersproject/units"; | ||
|
||
const contractName = "{{contractName}}"; | ||
|
||
const main = async (args: any, hre: HardhatRuntimeEnvironment) => { | ||
const [signer] = await hre.ethers.getSigners(); | ||
console.log(`🔑 Using account: ${signer.address}\n`); | ||
|
||
const factory = await hre.ethers.getContractFactory(contractName); | ||
const contract = factory.attach(args.contract); | ||
|
||
const tx = await contract | ||
.connect(signer) | ||
.sendHelloWorld(args.destination, { value: parseEther(args.amount) }); | ||
|
||
const receipt = await tx.wait(); | ||
console.log(`✅ "sendHelloWorld" transaction has been broadcasted to ${hre.network.name} | ||
📝 Transaction hash: ${receipt.transactionHash} | ||
|
||
Please, refer to ZetaChain's explorer for updates on the progress of the cross-chain transaction. | ||
|
||
🌍 Explorer: https://explorer.zetachain.com/cc/tx/${receipt.transactionHash} | ||
`); | ||
}; | ||
|
||
task("interact", "Sends a message from one chain to another.", main) | ||
.addParam("contract", "Contract address") | ||
.addParam("amount", "Token amount to send") | ||
.addParam("destination", "Destination chain ID (integer)"); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this, I recommend using
plop
as we did in the past (much more maintainable and extensible): https://github.com/zeta-chain/zeta-examples/blob/498e3d51b48263184614c94704251c5839fde8a7/plopfile.js#L1.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think this code could be refactored to use a library. I've looked into
plop
and it's a good candidate, however, right now there are issues with using it with TS plopjs/plop#297Created an issue to track the refactor: #15
This refactor is a nice to have and shouldn't block the PR, I think.