Skip to content

Instantly share code, notes, and snippets.

@solangegueiros
Last active December 9, 2023 13:13
Show Gist options
  • Save solangegueiros/97b2208cf3d97e107c18cfe8bd73ae37 to your computer and use it in GitHub Desktop.
Save solangegueiros/97b2208cf3d97e107c18cfe8bd73ae37 to your computer and use it in GitHub Desktop.
ccip-cross-chain-nft-game
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// End consumer library.
library Client {
struct EVMTokenAmount {
address token; // token address on the local chain.
uint256 amount; // Amount of tokens.
}
struct Any2EVMMessage {
bytes32 messageId; // MessageId corresponding to ccipSend on source.
uint64 sourceChainSelector; // Source chain selector.
bytes sender; // abi.decode(sender) if coming from an EVM chain.
bytes data; // payload sent in original message.
EVMTokenAmount[] destTokenAmounts; // Tokens and their amounts in their destination chain representation.
}
// If extraArgs is empty bytes, the default is 200k gas limit and strict = false.
struct EVM2AnyMessage {
bytes receiver; // abi.encode(receiver address) for dest EVM chains
bytes data; // Data payload
EVMTokenAmount[] tokenAmounts; // Token transfers
address feeToken; // Address of feeToken. address(0) means you will send msg.value.
bytes extraArgs; // Populate this with _argsToBytes(EVMExtraArgsV1)
}
// extraArgs will evolve to support new features
// bytes4(keccak256("CCIP EVMExtraArgsV1"));
bytes4 public constant EVM_EXTRA_ARGS_V1_TAG = 0x97a657c9;
struct EVMExtraArgsV1 {
uint256 gasLimit; // ATTENTION!!! MAX GAS LIMIT 4M FOR BETA TESTING
bool strict; // See strict sequencing details below.
}
function _argsToBytes(EVMExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) {
return abi.encodeWithSelector(EVM_EXTRA_ARGS_V1_TAG, extraArgs);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
// Importing OpenZeppelin contracts
import "@openzeppelin/contracts@4.6.0/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts@4.6.0/utils/Counters.sol";
import "@openzeppelin/contracts@4.6.0/utils/Base64.sol";
// Importing Chainlink contracts
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract CrossChainGameNFT is ERC721, ERC721URIStorage {
using Counters for Counters.Counter;
using Strings for uint256;
Counters.Counter public tokenIdCounter;
// Create price feed
AggregatorV3Interface internal priceFeed;
uint256 public lastPrice = 0;
string priceIndicatorUp = unicode"😀";
string priceIndicatorDown = unicode"😔";
string priceIndicatorFlat = unicode"😑";
string public priceIndicator;
struct ChainStruct {
uint64 code;
string name;
string color;
}
mapping (uint256 => ChainStruct) chain;
//https://docs.chain.link/ccip/supported-networks/testnet
constructor() ERC721("CrossChain Game", "CCIPG") {
chain[0] = ChainStruct ({
code: 16015286601757825753,
name: "Sepolia",
color: "#0000ff" //blue
});
chain[1] = ChainStruct ({
code: 14767482510784806043,
name: "Fuji",
color: "#ff0000" //red
});
chain[2] = ChainStruct ({
code: 12532609583862916517,
name: "Mumbai",
color: "#4b006e" //purple
});
//https://docs.chain.link/data-feeds/price-feeds/addresses
priceFeed = AggregatorV3Interface(
// Mumbai BTC/USD
0x007A22900a3B98143368Bd5906f8E17e9867581b
);
// Mint an NFT - source Mumbai
mint(msg.sender);
}
function mint(address to) public {
mintFrom(to, 2);
}
function mintFrom(address to, uint256 sourceId) public {
// sourceId 0 Sepolia, 1 Fuji, 2 Mumbai
uint256 tokenId = tokenIdCounter.current();
_safeMint(to, tokenId);
updateMetaData(tokenId, sourceId);
tokenIdCounter.increment();
}
// Update MetaData
function updateMetaData(uint256 tokenId, uint256 sourceId) public {
// Create the SVG string
string memory finalSVG = buildSVG(sourceId);
// Base64 encode the SVG
string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "Cross-chain SVG",',
'"description": "SVG NFTs in different chains",',
'"image": "data:image/svg+xml;base64,',
Base64.encode(bytes(finalSVG)), '",',
'"attributes": [',
'{"trait_type": "source",',
'"value": "', chain[sourceId].name ,'"},',
'{"trait_type": "price",',
'"value": "', lastPrice.toString() ,'"}',
']}'
)
)
)
);
// Create token URI
string memory finalTokenURI = string(
abi.encodePacked("data:application/json;base64,", json)
);
// Set token URI
_setTokenURI(tokenId, finalTokenURI);
}
// Build the SVG string
function buildSVG(uint256 sourceId) internal returns (string memory) {
// Create SVG rectangle with random color
string memory headSVG = string(
abi.encodePacked(
"<svg xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns:svgjs='http://svgjs.com/svgjs' width='500' height='500' preserveAspectRatio='none' viewBox='0 0 500 500'> <rect width='100%' height='100%' fill='",
chain[sourceId].color,
"' />"
)
);
// Update emoji based on price
string memory bodySVG = string(
abi.encodePacked(
"<text x='50%' y='50%' font-size='128' dominant-baseline='middle' text-anchor='middle'>",
comparePrice(),
"</text>"
)
);
// Close SVG
string memory tailSVG = "</svg>";
// Concatenate SVG strings
string memory _finalSVG = string(
abi.encodePacked(headSVG, bodySVG, tailSVG)
);
return _finalSVG;
}
// Compare new price to previous price
function comparePrice() public returns (string memory) {
uint256 currentPrice = getChainlinkDataFeedLatestAnswer();
if (currentPrice > lastPrice) {
priceIndicator = priceIndicatorUp;
} else if (currentPrice < lastPrice) {
priceIndicator = priceIndicatorDown;
} else {
priceIndicator = priceIndicatorFlat;
}
lastPrice = currentPrice;
return priceIndicator;
}
function getChainlinkDataFeedLatestAnswer() public view returns (uint256) {
(, int256 price, , , ) = priceFeed.latestRoundData();
return uint256(price);
}
// The following function is an override required by Solidity.
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage)
{
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public view override(ERC721, ERC721URIStorage) returns (string memory)
{
return super.tokenURI(tokenId);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
interface InftMinter {
function mintFrom(address account, uint256 sourceId) external;
}
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract CrossDestinationMinter is CCIPReceiver {
InftMinter public nft;
event MintCallSuccessfull();
// https://docs.chain.link/ccip/supported-networks/testnet#polygon-mumbai
address routerMumbai = 0x70499c328e1E2a3c41108bd3730F6670a44595D1;
constructor(address nftAddress) CCIPReceiver(routerMumbai) {
nft = InftMinter(nftAddress);
}
function _ccipReceive(
Client.Any2EVMMessage memory message
) internal override {
(bool success, ) = address(nft).call(message.data);
require(success);
emit MintCallSuccessfull();
}
function testMint() external {
nft.mintFrom(msg.sender, 2);
}
function testMessage() external {
bytes memory message;
message = abi.encodeWithSignature("mintFrom(address,uint256)", msg.sender, 2);
(bool success, ) = address(nft).call(message);
require(success);
emit MintCallSuccessfull();
}
function updateNFT(address nftAddress) external {
nft = InftMinter(nftAddress);
}
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract CrossSourceMinter {
// Custom errors to provide more descriptive revert messages.
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance to cover the fees.
error NothingToWithdraw(); // Used when trying to withdraw but there's nothing to withdraw.
IRouterClient public router;
LinkTokenInterface public linkToken;
uint64 public destinationChainSelector;
address public owner;
address public destinationMinter;
event MessageSent(bytes32 messageId);
constructor(address destMinterAddress) {
owner = msg.sender;
// from Fuji
// https://docs.chain.link/ccip/supported-networks#avalanche-fuji
address routerAddressFuji = 0x554472a2720E5E7D5D3C817529aBA05EEd5F82D8;
router = IRouterClient(routerAddressFuji);
linkToken = LinkTokenInterface(0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846);
linkToken.approve(routerAddressFuji, type(uint256).max);
// to Mumbai
// https://docs.chain.link/ccip/supported-networks/testnet#polygon-mumbai
destinationChainSelector = 12532609583862916517;
destinationMinter = destMinterAddress;
}
function mintOnMumbai() external {
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(destinationMinter),
data: abi.encodeWithSignature("mintFrom(address,uint256)", msg.sender, 1),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 980_000, strict: false})
),
feeToken: address(linkToken)
});
// Get the fee required to send the message
uint256 fees = router.getFee(destinationChainSelector, message);
if (fees > linkToken.balanceOf(address(this)))
revert NotEnoughBalance(linkToken.balanceOf(address(this)), fees);
bytes32 messageId;
// Send the message through the router and store the returned message ID
messageId = router.ccipSend(destinationChainSelector, message);
emit MessageSent(messageId);
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function linkBalance (address account) public view returns (uint256) {
return linkToken.balanceOf(account);
}
function withdrawLINK(
address beneficiary
) public onlyOwner {
uint256 amount = linkToken.balanceOf(address(this));
if (amount == 0) revert NothingToWithdraw();
linkToken.transfer(beneficiary, amount);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment