Skip to content

Commit

Permalink
Merge pull request #20 from onflow/add-serialization
Browse files Browse the repository at this point in the history
Add NFT serialization
  • Loading branch information
sisyphusSmiling authored Apr 3, 2024
2 parents 5ebf96c + a17d812 commit 1ed2dab
Show file tree
Hide file tree
Showing 24 changed files with 936 additions and 74 deletions.
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

0 comments on commit 1ed2dab

Please sign in to comment.