Skip to content
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

Add NFT serialization #20

Merged
merged 25 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
aeded2f
add Serialize util contract to support URL encoding serialized metadata
sisyphusSmiling Mar 7, 2024
f07dd22
update Serialize util contract
sisyphusSmiling Mar 8, 2024
324ddb0
add Serialize test cases
sisyphusSmiling Mar 15, 2024
8878a76
fix array serialization
sisyphusSmiling Mar 15, 2024
98e50a9
update ExampleNFT Traits for serialization testability
sisyphusSmiling Mar 15, 2024
487fce7
update serialization utils & tests
sisyphusSmiling Mar 18, 2024
001674b
update foundry ci workflow
sisyphusSmiling Mar 18, 2024
d08ff74
add Cadence tests to CI workflow
sisyphusSmiling Mar 18, 2024
59be19d
update Cadence tests to run on PR
sisyphusSmiling Mar 18, 2024
f4f0e21
update Flow CLI version used for CI
sisyphusSmiling Mar 18, 2024
c34d6fd
fix test script command
sisyphusSmiling Mar 18, 2024
9e649c4
add NFT serialization into bridge to EVM
sisyphusSmiling Mar 18, 2024
969e079
add metadata syncing on tokenURI when bridging from EVM to Cadence
sisyphusSmiling Mar 18, 2024
de837be
update serialize_tests.cdc
sisyphusSmiling Mar 18, 2024
90210f7
fix conformance errors & update setup commands
sisyphusSmiling Mar 19, 2024
85bec37
update serialization tests
sisyphusSmiling Mar 19, 2024
ebcabf8
minimize serialization utils by removing interfaces & strategies
sisyphusSmiling Mar 19, 2024
86735b5
fix NFT serialization, add test cases & add contract URI serializatio…
sisyphusSmiling Mar 20, 2024
8c3b445
add test/get_block_height script
sisyphusSmiling Mar 20, 2024
ad2c319
fix foundry_test ci failures
sisyphusSmiling Mar 20, 2024
2757b83
update serialization serialized json data prefix
sisyphusSmiling Mar 20, 2024
886a718
Update flow-cli version used in CI workflow
m-Peter Mar 20, 2024
eb43517
remove unnecessary script
sisyphusSmiling Mar 20, 2024
3c57f2e
update test comments
sisyphusSmiling Mar 21, 2024
a17d812
remove unused contract
sisyphusSmiling Mar 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/cadence_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on: pull_request

jobs:
tests:
name: Flow CLI Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.20.x'
- uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install Flow CLI
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" -- v1.15.0-cadence-v1.0.0-preview.12
- name: Flow CLI Version
run: flow version
- name: Update PATH
run: echo "/root/.local/bin" >> $GITHUB_PATH
- name: Run tests
run: sh local/run_cadence_tests.sh
- name: Normalize coverage report filepaths
run : sh ./local/normalize_coverage_report.sh
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: test

on: workflow_dispatch
on: pull_request

env:
FOUNDRY_PROFILE: ci
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ docs/
# Dotenv file
.env

# flow-evm-gateway db/ files
db/

# Cadence test framework coverage
coverage.json
coverage.lcov
9 changes: 6 additions & 3 deletions cadence/args/bridged-nft-code-chunks-args.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cadence/args/deploy-factory-args.json

Large diffs are not rendered by default.

74 changes: 57 additions & 17 deletions cadence/contracts/bridge/FlowEVMBridge.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import "FlowEVMBridgeConfig"
import "FlowEVMBridgeUtils"
import "FlowEVMBridgeNFTEscrow"
import "FlowEVMBridgeTemplates"
import "SerializeNFT"

/// The FlowEVMBridge contract is the main entrypoint for bridging NFT & FT assets between Flow & FlowEVM.
///
Expand Down Expand Up @@ -135,10 +136,15 @@ contract FlowEVMBridge : IFlowEVMNFTBridge {
let tokenType = token.getType()
let tokenID = token.id
let evmID = CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) ?? UInt256(token.id)

// Grab the URI from the NFT if available
var uri: String = ""
// Default to project-specified URI
if let metadata = token.resolveView(Type<CrossVMNFT.EVMBridgedMetadata>()) as! CrossVMNFT.EVMBridgedMetadata? {
uri = metadata.uri.uri()
} else {
// Otherwise, serialize the NFT
uri = SerializeNFT.serializeNFTMetadataAsURI(&token as &{NonFungibleToken.NFT})
}

// Lock the NFT & calculate the storage used by the NFT
Expand All @@ -159,6 +165,7 @@ contract FlowEVMBridge : IFlowEVMNFTBridge {
let isFactoryDeployed = FlowEVMBridgeUtils.isEVMContractBridgeOwned(evmContractAddress: associatedAddress)
// Controlled by the bridge - mint or transfer based on existence
if isFactoryDeployed {

// Check if the ERC721 exists
let existsResponse = EVM.decodeABI(
types: [Type<Bool>()],
Expand All @@ -173,17 +180,27 @@ contract FlowEVMBridge : IFlowEVMNFTBridge {
assert(existsResponse.length == 1, message: "Invalid response length")
let exists = existsResponse[0] as! Bool
if exists {
// if so transfer
let callResult: EVM.Result = FlowEVMBridgeUtils.call(
// If so transfer
let transferResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "safeTransferFrom(address,address,uint256)",
targetEVMAddress: associatedAddress,
args: [self.getBridgeCOAEVMAddress(), to, evmID],
gasLimit: 15000000,
value: 0.0
)
assert(callResult.status == EVM.Status.successful, message: "Tranfer to bridge recipient failed")
assert(transferResult.status == EVM.Status.successful, message: "Tranfer to bridge recipient failed")

// And update the URI to reflect current metadata
let updateURIResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "updateTokenURI(uint256,string)",
targetEVMAddress: associatedAddress,
args: [evmID, uri],
gasLimit: 15000000,
value: 0.0
)
assert(updateURIResult.status == EVM.Status.successful, message: "Tranfer to bridge recipient failed")
} else {
// Otherwise mint
// Otherwise mint with current URI
let callResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "safeMint(address,uint256,string)",
targetEVMAddress: associatedAddress,
Expand All @@ -208,7 +225,7 @@ contract FlowEVMBridge : IFlowEVMNFTBridge {

/// Public entrypoint to bridge NFTs from EVM to Cadence
///
/// @param owner: The EVM address of the NFT owner. Current ownership and successful transfer (via
/// @param owner: The EVM address of the NFT owner. Current ownership and successful transfer (via
/// `protectedTransferCall`) is validated before the bridge request is executed.
/// @param calldata: Caller-provided approve() call, enabling contract COA to operate on NFT in EVM contract
/// @param id: The NFT ID to bridged
Expand Down Expand Up @@ -241,7 +258,7 @@ contract FlowEVMBridge : IFlowEVMNFTBridge {
// Get the EVMAddress of the ERC721 contract associated with the type
let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type)
?? panic("No EVMAddress found for token type")

// Ensure the caller is either the current owner or approved for the NFT
let isAuthorized: Bool = FlowEVMBridgeUtils.isOwnerOrApproved(
ofNFT: id,
Expand All @@ -261,18 +278,28 @@ contract FlowEVMBridge : IFlowEVMNFTBridge {
evmContractAddress: associatedAddress
)
assert(isEscrowed, message: "Transfer to bridge COA failed - cannot bridge NFT without bridge escrow")

// Derive the defining Cadence contract name & address & attempt to borrow it as IEVMBridgeNFTMinter
let contractName = FlowEVMBridgeUtils.getContractName(fromType: type)!
let contractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: type)!
let nftContract = getAccount(contractAddress).contracts.borrow<&{IEVMBridgeNFTMinter}>(name: contractName)
// Get the token URI from the ERC721 contract
let uri = FlowEVMBridgeUtils.getTokenURI(evmContractAddress: associatedAddress, id: id)
// If the NFT is currently locked, unlock and return
if let cadenceID = FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id) {
return <-FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: cadenceID)
let nft <- FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: cadenceID)

// If the NFT is bridge-defined, update the URI from the source ERC721 contract
if self.account.address == FlowEVMBridgeUtils.getContractAddress(fromType: type) {
nftContract!.updateTokenURI(evmID: id, newURI: uri)
}

return <-nft
}
// Otherwise, we expect the NFT to be minted in Cadence
let contractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: type)!
assert(self.account.address == contractAddress, message: "Unexpected error bridging NFT from EVM")

let contractName = FlowEVMBridgeUtils.getContractName(fromType: type)!
let nftContract = getAccount(contractAddress).contracts.borrow<&{IEVMBridgeNFTMinter}>(name: contractName)!
let uri = FlowEVMBridgeUtils.getTokenURI(evmContractAddress: associatedAddress, id: id)
let nft <- nftContract.mintNFT(id: id, tokenURI: uri)
let nft <- nftContract!.mintNFT(id: id, tokenURI: uri)
return <-nft
}

Expand Down Expand Up @@ -389,13 +416,26 @@ contract FlowEVMBridge : IFlowEVMNFTBridge {
// Borrow the ViewResolver to attempt to resolve the EVMBridgedMetadata view
let viewResolver = getAccount(cadenceAddress).contracts.borrow<&{ViewResolver}>(name: name)!
var contractURI = ""
if let bridgedMetadata = viewResolver.resolveContractView(
// Try to resolve the EVMBridgedMetadata
let bridgedMetadata = viewResolver.resolveContractView(
resourceType: forNFTType,
viewType: Type<CrossVMNFT.EVMBridgedMetadata>()
) as! CrossVMNFT.EVMBridgedMetadata? {
name = bridgedMetadata.name
symbol = bridgedMetadata.symbol
contractURI = bridgedMetadata.uri.uri()
) as! CrossVMNFT.EVMBridgedMetadata?
// Default to project-defined URI if available
if bridgedMetadata != nil {
name = bridgedMetadata!.name
symbol = bridgedMetadata!.symbol
contractURI = bridgedMetadata!.uri.uri()
} else {
// Otherwise, serialize collection-level NFTCollectionDisplay
if let collectionDisplay = viewResolver.resolveContractView(
resourceType: forNFTType,
viewType: Type<MetadataViews.NFTCollectionDisplay>()
) as! MetadataViews.NFTCollectionDisplay? {
name = collectionDisplay.name
let serializedDisplay = SerializeNFT.serializeFromDisplays(nftDisplay: nil, collectionDisplay: collectionDisplay)!
contractURI = "data:application/json;utf8,{".concat(serializedDisplay).concat("}")
}
}

// Call to the factory contract to deploy an ERC721
Expand Down
7 changes: 7 additions & 0 deletions cadence/contracts/bridge/IEVMBridgeNFTMinter.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,11 @@ contract interface IEVMBridgeNFTMinter {
///
access(account)
fun mintNFT(id: UInt256, tokenURI: String): @{NonFungibleToken.NFT}

/// Allows the bridge to update the URI of bridged NFTs. This assumes that the EVM-defining project may contain
/// logic (onchain or offchain) which updates NFT metadata in the source ERC721 contract. On bridging, the URI can
/// then be updated in this contract to reflect the source ERC721 contract's metadata.
///
access(account)
fun updateTokenURI(evmID: UInt256, newURI: String)
}
4 changes: 0 additions & 4 deletions cadence/contracts/example-assets/ExampleNFT.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,6 @@ access(all) contract ExampleNFT: NonFungibleToken {
let excludedTraits = ["mintedTime", "foo"]
let traitsView = MetadataViews.dictToTraits(dict: self.metadata, excludedNames: excludedTraits)

// mintedTime is a unix timestamp, we should mark it with a displayType so platforms know how to show it.
let mintedTimeTrait = MetadataViews.Trait(name: "mintedTime", value: self.metadata["mintedTime"]!, displayType: "Date", rarity: nil)
traitsView.addTrait(mintedTimeTrait)

// foo is a trait with its own rarity
let fooTraitRarity = MetadataViews.Rarity(score: 10.0, max: 100.0, description: "Common")
let fooTrait = MetadataViews.Trait(name: "foo", value: self.metadata["foo"], displayType: nil, rarity: fooTraitRarity)
Expand Down
37 changes: 28 additions & 9 deletions cadence/contracts/templates/emulator/EVMBridgedNFTTemplate.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ access(all) contract {{CONTRACT_NAME}} : ICrossVM, IEVMBridgeNFTMinter, NonFungi
access(all) var contractURI: String?
/// Retain a Collection to reference when resolving Collection Metadata
access(self) let collection: @Collection
/// Mapping of token URIs indexed on their ERC721 ID. This would not normally be retained within a Cadence NFT
/// contract, but since NFT metadata may be updated in EVM, it's retained here so that the bridge can update
/// it against the source ERC721 contract which is treated as the NFT's source of truth.
access(all) let tokenURIs: {UInt256: String}

/// The NFT resource representing the bridged ERC721 token
///
Expand All @@ -54,23 +58,19 @@ access(all) contract {{CONTRACT_NAME}} : ICrossVM, IEVMBridgeNFTMinter, NonFungi
access(all) let name: String
/// The symbol of the NFT as defined in the ERC721 contract
access(all) let symbol: String
/// The URI of the NFT as defined in the ERC721 contract
access(all) let uri: String
/// Additional onchain metadata
access(all) let metadata: {String: AnyStruct}

init(
name: String,
symbol: String,
evmID: UInt256,
uri: String,
metadata: {String: AnyStruct}
) {
self.name = name
self.symbol = symbol
self.id = self.uuid
self.evmID = evmID
self.uri = uri
self.metadata = metadata
}

Expand All @@ -93,7 +93,7 @@ access(all) contract {{CONTRACT_NAME}} : ICrossVM, IEVMBridgeNFTMinter, NonFungi
return CrossVMNFT.EVMBridgedMetadata(
name: self.name,
symbol: self.symbol,
uri: CrossVMNFT.URI(self.tokenURI())
uri: CrossVMNFT.URI(baseURI: nil, value: self.tokenURI())
)
case Type<MetadataViews.Serial>():
return MetadataViews.Serial(
Expand Down Expand Up @@ -127,7 +127,7 @@ access(all) contract {{CONTRACT_NAME}} : ICrossVM, IEVMBridgeNFTMinter, NonFungi

/// Similar to ERC721.tokenURI method, returns the URI of the NFT with self.evmID at time of bridging
access(all) view fun tokenURI(): String {
return self.uri
return {{CONTRACT_NAME}}.tokenURIs[self.evmID] ?? ""
}
}

Expand Down Expand Up @@ -320,7 +320,7 @@ access(all) contract {{CONTRACT_NAME}} : ICrossVM, IEVMBridgeNFTMinter, NonFungi
return CrossVMNFT.EVMBridgedMetadata(
name: self.name,
symbol: self.symbol,
uri: self.contractURI != nil ? CrossVMNFT.URI(self.contractURI!) : CrossVMNFT.URI("")
uri: self.contractURI != nil ? CrossVMNFT.URI(baseURI: nil, value: self.contractURI!) : CrossVMNFT.URI(baseURI: nil, value: "")
)
}
return nil
Expand All @@ -330,27 +330,46 @@ access(all) contract {{CONTRACT_NAME}} : ICrossVM, IEVMBridgeNFTMinter, NonFungi
Internal Methods
***********************/

/// Allows the bridge to
/// Allows the bridge to mint NFTs from bridge-defined NFT contracts
///
access(account)
fun mintNFT(id: UInt256, tokenURI: String): @NFT {
pre {
self.tokenURIs[id] == nil: "A token with the given ERC721 ID already exists"
}
self.tokenURIs[id] = tokenURI
return <-create NFT(
name: self.name,
symbol: self.symbol,
evmID: id,
uri: tokenURI,
metadata: {
"Bridged Block": getCurrentBlock().height,
"Bridged Timestamp": getCurrentBlock().timestamp
}
)
}

/// Allows the bridge to update the URI of bridged NFTs. This assumes that the EVM-defining project may contain
/// logic (onchain or offchain) which updates NFT metadata in the source ERC721 contract. On bridging, the URI can
/// then be updated in this contract to reflect the source ERC721 contract's metadata.
///
access(account)
fun updateTokenURI(evmID: UInt256, newURI: String) {
pre {
self.tokenURIs[evmID] != nil: "No token with the given ERC721 ID exists"
}
if self.tokenURIs[evmID] != newURI {
self.tokenURIs[evmID] = newURI
}
}

init(name: String, symbol: String, evmContractAddress: EVM.EVMAddress, contractURI: String?) {
self.evmNFTContractAddress = evmContractAddress
self.flowNFTContractAddress = self.account.address
self.name = name
self.symbol = symbol
self.contractURI = contractURI
self.tokenURIs = {}
self.collection <- create Collection()

FlowEVMBridgeConfig.associateType(Type<@{{CONTRACT_NAME}}.NFT>(), with: self.evmNFTContractAddress)
Expand Down
Loading
Loading