Skip to content

Commit

Permalink
[framework] use NFT<T> standard in cross-chain airdrop
Browse files Browse the repository at this point in the history
  • Loading branch information
sblackshear committed Mar 15, 2022
1 parent 9527322 commit 368a434
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 58 deletions.
10 changes: 7 additions & 3 deletions sui/src/unit_tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,14 @@ async fn test_cross_chain_airdrop() -> Result<(), anyhow::Error> {
let token = airdrop_call_move_and_get_created_object(args, gas_object_id, &mut context).await?;

// Verify the airdrop token
assert_eq!(token["contents"]["type"], ("0x2::CrossChainAirdrop::NFT"));
let fields = &token["contents"]["fields"];
assert_eq!(
fields["source_token_id"]["fields"]["id"],
token["contents"]["type"],
("0x2::NFT::NFT<0x2::CrossChainAirdrop::ERC721>")
);
let nft_data = &token["contents"]["fields"]["data"];
let erc721_metadata = &nft_data["fields"]["metadata"];
assert_eq!(
erc721_metadata["fields"]["token_id"]["fields"]["id"],
AIRDROP_SOURCE_TOKEN_ID
);

Expand Down
54 changes: 20 additions & 34 deletions sui_programmability/framework/sources/CrossChainAirdrop.move
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,17 @@
/// chains by adding a chain_id field.
module Sui::CrossChainAirdrop {
use Std::Vector;
use Sui::ERC721Metadata::{Self, ERC721Metadata, TokenID};
use Sui::ID::{VersionedID};
use Sui::NFT;
use Sui::Transfer;
use Sui::TxContext::{Self, TxContext};

/// The oracle manages one `PerContractAirdropInfo` for each Ethereum contract
struct CrossChainAirdropOracle has key {
id: VersionedID,
// TODO: replace this with SparseSet for O(1) on-chain uniqueness check
managed_contracts: vector<PerContractAirdropInfo>
}

/// The token ID on the source contract
struct SourceTokenID has store, copy {
id: u64,
managed_contracts: vector<PerContractAirdropInfo>,
}

/// The address of the source contract
Expand All @@ -38,26 +35,15 @@ module Sui::CrossChainAirdrop {
// TODO: replace u64 with u256 once the latter is supported
// <https://github.com/MystenLabs/fastnft/issues/618>
// TODO: replace this with SparseSet for O(1) on-chain uniqueness check
claimed_source_token_ids: vector<SourceTokenID>
claimed_source_token_ids: vector<TokenID>
}

/// The Sui representation of the original NFT
struct NFT has key {
id: VersionedID,
/// The Sui representation of the original ERC721 NFT on Eth
struct ERC721 has store {
/// The address of the source contract, e.g, the Ethereum contract address
source_contract_address: SourceContractAddress,
// TODO: replace u64 with u256 once the latter is supported
// <https://github.com/MystenLabs/fastnft/issues/618>
/// The token id associated with the source contract e.g., the Ethereum token id
source_token_id: SourceTokenID,
/// A distinct Uniform Resource Identifier (URI) for a given asset.
/// This corresponds to the `tokenURI()` method in the ERC721Metadata
/// interface in EIP-721.
token_uri: vector<u8>,
/// A descriptive name for a collection of NFTs in this contract.
/// This corresponds to the `name()` method in the
/// ERC721Metadata interface in EIP-721.
name: vector<u8>,
/// The metadata associated with this NFT
metadata: ERC721Metadata,
}

/// Address of the Oracle
Expand Down Expand Up @@ -92,18 +78,18 @@ module Sui::CrossChainAirdrop {
ctx: &mut TxContext,
) {
let contract = get_or_create_contract(oracle, &source_contract_address);
let token_id = ERC721Metadata::new_token_id(source_token_id);
// NOTE: this is where the globally uniqueness check happens
assert!(!is_token_claimed(contract, source_token_id), ETOKEN_ID_CLAIMED);
let token_id = SourceTokenID{ id: source_token_id };
let coin = NFT {
id: TxContext::new_id(ctx),
source_contract_address: SourceContractAddress { address: source_contract_address },
source_token_id: copy token_id,
name,
token_uri
};
assert!(!is_token_claimed(contract, &token_id), ETOKEN_ID_CLAIMED);
let nft = NFT::mint(
ERC721 {
source_contract_address: SourceContractAddress { address: source_contract_address },
metadata: ERC721Metadata::new(token_id, name, token_uri),
},
ctx
);
Vector::push_back(&mut contract.claimed_source_token_ids, token_id);
Transfer::transfer(coin, recipient);
Transfer::transfer(nft, recipient)
}

fun get_or_create_contract(oracle: &mut CrossChainAirdropOracle, source_contract_address: &vector<u8>): &mut PerContractAirdropInfo {
Expand All @@ -130,12 +116,12 @@ module Sui::CrossChainAirdrop {
Vector::borrow_mut(&mut oracle.managed_contracts, idx)
}

fun is_token_claimed(contract: &PerContractAirdropInfo, source_token_id: u64): bool {
fun is_token_claimed(contract: &PerContractAirdropInfo, source_token_id: &TokenID): bool {
// TODO: replace this with SparseSet so that the on-chain uniqueness check can be O(1)
let index = 0;
while (index < Vector::length(&contract.claimed_source_token_ids)) {
let claimed_id = Vector::borrow(&contract.claimed_source_token_ids, index);
if (&claimed_id.id == &source_token_id) {
if (claimed_id == source_token_id) {
return true
};
index = index + 1;
Expand Down
55 changes: 55 additions & 0 deletions sui_programmability/framework/sources/ERC721Metadata.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module Sui::ERC721Metadata {
use Std::ASCII;
use Sui::Url::{Self, Url};
use Sui::UTF8;

// TODO: add symbol()?
/// A wrapper type for the ERC721 metadata standard https://eips.ethereum.org/EIPS/eip-721
struct ERC721Metadata has store {
/// The token id associated with the source contract on Ethereum
token_id: TokenID,
/// A descriptive name for a collection of NFTs in this contract.
/// This corresponds to the `name()` method in the
/// ERC721Metadata interface in EIP-721.
name: UTF8::String,
/// A distinct Uniform Resource Identifier (URI) for a given asset.
/// This corresponds to the `tokenURI()` method in the ERC721Metadata
/// interface in EIP-721.
token_uri: Url,
}

// TODO: replace u64 with u256 once the latter is supported
// <https://github.com/MystenLabs/fastnft/issues/618>
/// An ERC721 token ID
struct TokenID has store, copy {
id: u64,
}

/// Construct a new ERC721Metadata from the given inputs. Does not perform any validation
/// on `token_uri` or `name`
public fun new(token_id: TokenID, name: vector<u8>, token_uri: vector<u8>): ERC721Metadata {
// Note: this will abort if `token_uri` is not valid ASCII
let uri_str = ASCII::string(token_uri);
ERC721Metadata {
token_id,
name: UTF8::string_unsafe(name),
token_uri: Url::new_unsafe(uri_str),
}
}

public fun new_token_id(id: u64): TokenID {
TokenID { id }
}

public fun token_id(self: &ERC721Metadata): &TokenID {
&self.token_id
}

public fun token_uri(self: &ERC721Metadata): &Url {
&self.token_uri
}

public fun name(self: &ERC721Metadata): &UTF8::String {
&self.name
}
}
30 changes: 15 additions & 15 deletions sui_programmability/framework/sources/NFT.move
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,27 @@ module Sui::NFT {
use Sui::Transfer;
use Sui::TxContext::{Self, TxContext};

/// An ERC721-like non-fungible token with mutable custom metadata.
/// The custom metadata must be provided by a separate module instantiating
/// An ERC721-like non-fungible token with mutable custom data.
/// The custom data must be provided by a separate module instantiating
/// this struct with a particular `T`. We will henceforth refer to the author
/// of this module as "the creator".
struct NFT<T: store> has key, store {
id: VersionedID,
/// Mutable custom metadata to be defined elsewhere
metadata: T,
data: T,
}

/// Create a new NFT with the given metadata.
/// Create a new NFT with the given data.
/// It is the creator's responsibility to restrict access
/// to minting. The recommended mechanism for this is to restrict
/// the ability to mint a value of type `T`--e.g., if the
/// the creator intends for only 10 `NFT<T>`'s to be minted
/// the code for creating `T` should maintain a counter to
/// enforce this.
public fun mint<T: store>(
metadata: T, ctx: &mut TxContext
data: T, ctx: &mut TxContext
): NFT<T> {
NFT { id: TxContext::new_id(ctx), metadata }
NFT { id: TxContext::new_id(ctx), data }
}

/// Burn `nft` and return its medatada.
Expand All @@ -34,29 +34,29 @@ module Sui::NFT {
/// burn fee of 10 coins, the code for collecting this
/// fee should gate the destruction of `T`.
public fun burn<T: store>(nft: NFT<T>): T {
let NFT { id, metadata } = nft;
let NFT { id, data } = nft;
ID::delete(id);
metadata
data
}

/// Send NFT to `recipient`
public fun transfer<T: store>(nft: NFT<T>, recipient: address) {
Transfer::transfer(nft, recipient)
}

/// Get an immutable reference to `nft`'s metadata
public fun metadata<T: store>(nft: &NFT<T>): &T {
&nft.metadata
/// Get an immutable reference to `nft`'s data
public fun data<T: store>(nft: &NFT<T>): &T {
&nft.data
}

/// Get a mutable reference to `nft`'s metadata.
/// If the creator wishes for the metadata to be immutable or
/// Get a mutable reference to `nft`'s data.
/// If the creator wishes for the data to be immutable or
/// enforce application-specific mutability policies on the
/// `T`, the recommended mechanism for this is to
/// - (1) avoid giving `T` the `drop` ability
/// - (2) enforce the policy inside the module that defines `T`
/// on a field-by-field basis.
public fun metadata_mut<T: store>(nft: &mut NFT<T>): &mut T {
&mut nft.metadata
public fun data_mut<T: store>(nft: &mut NFT<T>): &mut T {
&mut nft.data
}
}
37 changes: 37 additions & 0 deletions sui_programmability/framework/sources/UTF8.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module Sui::UTF8 {
use Std::ASCII;
use Std::Option::Option;

/// Wrapper type that should be interpreted as a UTF8 string by clients
struct String has store, copy, drop {
bytes: vector<u8>
}

// TODO: also include validating constructor
/// Construct a UTF8 string from `bytes`. Does not
/// perform any validation
public fun string_unsafe(bytes: vector<u8>): String {
String { bytes }
}

/// Construct a UTF8 string from the ASCII string `s`
public fun from_ascii(s: ASCII::String): String {
String { bytes: ASCII::into_bytes(s) }
}

/// Try to convert `self` to an ASCCI string
public fun try_into_ascii(self: String): Option<ASCII::String> {
ASCII::try_string(self.bytes)
}

/// Return the underyling bytes of `self`
public fun bytes(self: &String): &vector<u8> {
&self.bytes
}

/// Consume `self` and return its underlying bytes
public fun into_bytes(self: String): vector<u8> {
let String { bytes } = self;
bytes
}
}
2 changes: 1 addition & 1 deletion sui_programmability/framework/sources/Url.move
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ module Sui::Url {
// === constructors ===

/// Create a `Url`, with no validation
public fun new_unsafe_url(url: String): Url {
public fun new_unsafe(url: String): Url {
Url { url }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

#[test_only]
module Sui::CrossChainAirdropTests {
use Sui::CrossChainAirdrop::{Self, CrossChainAirdropOracle, NFT};
use Sui::CrossChainAirdrop::{Self, CrossChainAirdropOracle, ERC721};
use Sui::NFT::NFT;
use Sui::ID::{VersionedID};
use Sui::TestScenario::{Self, Scenario};

Expand Down Expand Up @@ -77,6 +78,6 @@ module Sui::CrossChainAirdropTests {
fun owns_object(scenario: &mut Scenario, owner: &address): bool{
// Verify the token has been transfer to the recipient
TestScenario::next_tx(scenario, owner);
TestScenario::can_remove_object<NFT>(scenario)
TestScenario::can_remove_object<NFT<ERC721>>(scenario)
}
}
6 changes: 3 additions & 3 deletions sui_programmability/framework/tests/UrlTests.move
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module Sui::UrlTests {
// url strings are not currently validated
let url_str = ASCII::string(x"414243454647");

let url = Url::new_unsafe_url(url_str);
let url = Url::new_unsafe(url_str);
assert!(Url::inner_url(&url) == url_str, URL_STRING_MISMATCH);
}

Expand All @@ -23,7 +23,7 @@ module Sui::UrlTests {
// length too short
let hash = x"badf012345";

let url = Url::new_unsafe_url(url_str);
let url = Url::new_unsafe(url_str);
let _ = Url::new_unsafe_url_commitment(url, hash);
}

Expand All @@ -34,7 +34,7 @@ module Sui::UrlTests {
// 32 bytes
let hash = x"1234567890123456789012345678901234567890abcdefabcdefabcdefabcdef";

let url = Url::new_unsafe_url(url_str);
let url = Url::new_unsafe(url_str);
let url_commit = Url::new_unsafe_url_commitment(url, hash);

assert!(Url::url_commitment_resource_hash(&url_commit) == hash, EHASH_LENGTH_MISMATCH);
Expand Down

0 comments on commit 368a434

Please sign in to comment.