Skip to content

Commit 9fe5abe

Browse files
committed
chore(docs): refactoring cross-chain tutorial
1 parent a2a6e5d commit 9fe5abe

File tree

9 files changed

+909
-178
lines changed

9 files changed

+909
-178
lines changed

docs/docs/developers/docs/tutorials/js_tutorials/token_bridge.md

Lines changed: 349 additions & 178 deletions
Large diffs are not rendered by default.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity >=0.8.27;
3+
4+
// docs:start:portal_setup
5+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
6+
import {IRegistry} from "@aztec/l1-contracts/src/governance/interfaces/IRegistry.sol";
7+
import {IInbox} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol";
8+
import {IOutbox} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol";
9+
import {IRollup} from "@aztec/l1-contracts/src/core/interfaces/IRollup.sol";
10+
import {DataStructures} from "@aztec/l1-contracts/src/core/libraries/DataStructures.sol";
11+
import {Hash} from "@aztec/l1-contracts/src/core/libraries/crypto/Hash.sol";
12+
13+
contract NFTPortal {
14+
IRegistry public registry;
15+
IERC721 public nftContract;
16+
bytes32 public l2Bridge;
17+
18+
IRollup public rollup;
19+
IOutbox public outbox;
20+
IInbox public inbox;
21+
uint256 public rollupVersion;
22+
23+
function initialize(address _registry, address _nftContract, bytes32 _l2Bridge) external {
24+
registry = IRegistry(_registry);
25+
nftContract = IERC721(_nftContract);
26+
l2Bridge = _l2Bridge;
27+
28+
rollup = IRollup(address(registry.getCanonicalRollup()));
29+
outbox = rollup.getOutbox();
30+
inbox = rollup.getInbox();
31+
rollupVersion = rollup.getVersion();
32+
}
33+
// docs:end:portal_setup
34+
35+
// docs:start:portal_deposit_and_withdraw
36+
// Lock NFT and send message to L2
37+
function depositToAztec(uint256 tokenId, bytes32 secretHash) external returns (bytes32, uint256) {
38+
// Lock the NFT
39+
nftContract.transferFrom(msg.sender, address(this), tokenId);
40+
41+
// Prepare L2 message - just a naive hash of our tokenId
42+
DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion);
43+
bytes32 contentHash = Hash.sha256ToField(abi.encode(tokenId));
44+
45+
// Send message to Aztec
46+
(bytes32 key, uint256 index) = inbox.sendL2Message(actor, contentHash, secretHash);
47+
return (key, index);
48+
}
49+
50+
// Unlock NFT after L2 burn
51+
function withdraw(
52+
uint256 tokenId,
53+
uint256 l2BlockNumber,
54+
uint256 leafIndex,
55+
bytes32[] calldata path
56+
) external {
57+
// Verify message from L2
58+
DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({
59+
sender: DataStructures.L2Actor(l2Bridge, rollupVersion),
60+
recipient: DataStructures.L1Actor(address(this), block.chainid),
61+
content: Hash.sha256ToField(abi.encodePacked(tokenId, msg.sender))
62+
});
63+
64+
outbox.consume(message, l2BlockNumber, leafIndex, path);
65+
66+
// Unlock NFT
67+
nftContract.transferFrom(address(this), msg.sender, tokenId);
68+
}
69+
}
70+
// docs:end:portal_deposit_and_withdraw
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// docs:start:simple_nft
3+
pragma solidity >=0.8.27;
4+
5+
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
6+
7+
contract SimpleNFT is ERC721 {
8+
uint256 private _currentTokenId;
9+
10+
constructor() ERC721("SimplePunk", "SPUNK") {}
11+
12+
function mint(address to) external returns (uint256) {
13+
uint256 tokenId = _currentTokenId++;
14+
_mint(to, tokenId);
15+
return tokenId;
16+
}
17+
}
18+
// docs:end:simple_nft
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[package]
2+
name = "nft"
3+
type = "contract"
4+
5+
[dependencies]
6+
aztec = { path = "../../../../noir-projects/aztec-nr/aztec" }
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// docs:start:contract_setup
2+
use aztec::macros::aztec;
3+
pub mod nft;
4+
5+
#[aztec]
6+
pub contract NFTPunk {
7+
use dep::aztec::{
8+
macros::{storage::storage, functions::{utility, private, public, initializer, internal}},
9+
protocol_types::{address::AztecAddress},
10+
state_vars::{PrivateSet, PublicImmutable, delayed_public_mutable::DelayedPublicMutable, Map}
11+
};
12+
use crate::nft::NFTNote;
13+
use dep::aztec::messages::message_delivery::MessageDelivery;
14+
use aztec::note::{note_getter_options::NoteGetterOptions, note_interface::NoteProperties, note_viewer_options::NoteViewerOptions};
15+
use aztec::utils::comparison::Comparator;
16+
17+
#[storage]
18+
struct Storage<Context> {
19+
admin: PublicImmutable<AztecAddress, Context>,
20+
minter: PublicImmutable<AztecAddress, Context>,
21+
nfts: Map<Field, DelayedPublicMutable<bool, 2, Context>, Context>,
22+
owners: Map<AztecAddress, PrivateSet<NFTNote, Context>, Context>,
23+
}
24+
#[public]
25+
#[initializer]
26+
fn constructor(admin: AztecAddress) {
27+
storage.admin.initialize(admin);
28+
}
29+
// docs:end:contract_setup
30+
31+
// docs:start:set_minter
32+
#[public]
33+
fn set_minter(minter: AztecAddress) {
34+
assert(storage.admin.read().eq(context.msg_sender().unwrap()), "caller is not admin");
35+
storage.minter.initialize(minter);
36+
}
37+
// docs:end:set_minter
38+
39+
// docs:start:mark_nft_exists
40+
#[public]
41+
#[internal]
42+
fn _mark_nft_exists(token_id: Field, exists: bool) {
43+
storage.nfts.at(token_id).schedule_value_change(exists);
44+
}
45+
// docs:end:mark_nft_exists
46+
47+
// docs:start:mint
48+
#[private]
49+
fn mint(to: AztecAddress, token_id: Field) {
50+
assert(storage.minter.read().eq(context.msg_sender().unwrap()), "caller is not the authorized minter");
51+
52+
// we create an NFT note and insert it to the PrivateSet - a collection of notes meant to be read in private
53+
let new_nft = NFTNote::new(to, token_id);
54+
storage.owners.at(to).insert(new_nft).emit(&mut context, to, MessageDelivery.CONSTRAINED_ONCHAIN);
55+
56+
// calling the internal public function above to indicate that the NFT is taken
57+
NFTPunk::at(context.this_address())._mark_nft_exists(token_id, true).enqueue(&mut context);
58+
}
59+
// docs:end:mint
60+
61+
// docs:start:notes_of
62+
#[utility]
63+
unconstrained fn notes_of(from: AztecAddress) -> Field {
64+
let notes = storage.owners.at(from).view_notes(NoteViewerOptions::new());
65+
notes.len() as Field
66+
}
67+
// docs:end:notes_of
68+
69+
// docs:start:burn
70+
#[private]
71+
fn burn(from: AztecAddress, token_id: Field) {
72+
assert(storage.minter.read().eq(context.msg_sender().unwrap()), "caller is not the authorized minter");
73+
74+
// from the NFTNote properties, selects token_id and compares it against the token_id to be burned
75+
let options = NoteGetterOptions::new().select(NFTNote::properties().token_id, Comparator.EQ, token_id).set_limit(1);
76+
let notes = storage.owners.at(from).pop_notes(options);
77+
assert(notes.len() == 1, "NFT not found");
78+
79+
NFTPunk::at(context.this_address())._mark_nft_exists(token_id, false).enqueue(&mut context);
80+
}
81+
// docs:end:burn
82+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// docs:start:nft_note_struct
2+
use dep::aztec::{
3+
macros::notes::note,
4+
protocol_types::{
5+
address::AztecAddress,
6+
traits::Packable,
7+
},
8+
oracle::random::random,
9+
};
10+
11+
#[derive(Eq, Packable)]
12+
#[note]
13+
pub struct NFTNote {
14+
owner: AztecAddress,
15+
randomness: Field,
16+
token_id: Field,
17+
}
18+
// docs:end:nft_note_struct
19+
20+
// docs:start:nft_note_new
21+
impl NFTNote {
22+
pub fn new(owner: AztecAddress, token_id: Field) -> Self {
23+
// The randomness preserves privacy by preventing brute-forcing
24+
NFTNote { owner, randomness: unsafe { random() }, token_id }
25+
}
26+
}
27+
// docs:end:nft_note_new
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
name = "nft_bridge"
3+
type = "contract"
4+
5+
[dependencies]
6+
aztec = { path = "../../../../noir-projects/aztec-nr/aztec" }
7+
NFTPunk = { path = "../nft" }
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// docs:start:bridge_setup
2+
use aztec::macros::aztec;
3+
4+
#[aztec]
5+
pub contract NFTBridge {
6+
use dep::aztec::{
7+
macros::{storage::storage, functions::{public, initializer, private}},
8+
protocol_types::{address::AztecAddress, address::EthAddress, hash::sha256_to_field},
9+
state_vars::{PublicImmutable},
10+
};
11+
use dep::NFTPunk::NFTPunk;
12+
13+
#[storage]
14+
struct Storage<Context> {
15+
nft: PublicImmutable<AztecAddress, Context>,
16+
portal: PublicImmutable<EthAddress, Context>,
17+
}
18+
19+
#[public]
20+
#[initializer]
21+
fn constructor(nft: AztecAddress) {
22+
storage.nft.initialize(nft);
23+
}
24+
25+
#[public]
26+
fn set_portal(portal: EthAddress) {
27+
storage.portal.initialize(portal);
28+
}
29+
// docs:end:bridge_setup
30+
31+
// docs:start:claim
32+
#[private]
33+
fn claim(to: AztecAddress, token_id: Field, secret: Field, message_leaf_index: Field) {
34+
// Compute the message hash that was sent from L1
35+
let token_id_bytes: [u8; 32] = (token_id as Field).to_be_bytes();
36+
let content_hash = sha256_to_field(token_id_bytes);
37+
38+
// Consume the L1 -> L2 message
39+
context.consume_l1_to_l2_message(
40+
content_hash,
41+
secret,
42+
storage.portal.read(),
43+
message_leaf_index
44+
);
45+
46+
// Mint the NFT on L2
47+
let nft = storage.nft.read();
48+
NFTPunk::at(nft).mint(to, token_id).call(&mut context);
49+
}
50+
// docs:end:claim
51+
52+
// docs:start:exit
53+
#[private]
54+
fn exit(
55+
token_id: Field,
56+
recipient: EthAddress
57+
) {
58+
// Create L2->L1 message to unlock NFT on L1
59+
let token_id_bytes: [u8; 32] = token_id.to_be_bytes();
60+
let recipient_bytes: [u8; 20] = recipient.to_be_bytes();
61+
let content = sha256_to_field(token_id_bytes.concat(recipient_bytes));
62+
context.message_portal(storage.portal.read(), content);
63+
64+
// Burn the NFT on L2
65+
let nft = storage.nft.read();
66+
NFTPunk::at(nft).burn(context.msg_sender().unwrap(), token_id).call(&mut context);
67+
}
68+
// docs:end:exit
69+
}

0 commit comments

Comments
 (0)