Last active
December 9, 2023 13:13
-
-
Save solangegueiros/97b2208cf3d97e107c18cfe8bd73ae37 to your computer and use it in GitHub Desktop.
ccip-cross-chain-nft-game
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
// 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); | |
} | |
} |
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
// 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); | |
} | |
} |
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
// 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); | |
} | |
} |
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
// 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